Magento performance is a caching stack, not one cache
People talk about "the Magento cache" as if it were a single thing. It is not. A well-configured store has several distinct caching layers, each doing a different job, and performance depends on all of them being set up correctly.
OPcache caches compiled PHP, Redis or Valkey caches application data and sessions, and Varnish caches whole pages. Miss any one, and the store does more work than it should on every request.
This article covers each layer, how they fit together on a real request, the common misconfigurations, and the order to set them up.
OPcache: compiled PHP in memory
The lowest layer is OPcache, which is part of PHP itself. It stores the compiled bytecode of PHP files so the engine does not recompile them on every request.
Magento is a very large PHP codebase, so this matters enormously. Without OPcache, every request recompiles thousands of files; with it, the compiled code is served from memory.
OPcache sits outside Magento's own cache commands entirely, which is exactly why it gets overlooked. A store can have every Magento cache enabled and still be slow because OPcache is undersized or misconfigured.
Sizing and configuring OPcache
OPcache needs enough memory to hold the compiled codebase. The opcache.memory_consumption and opcache.max_accelerated_files settings must be large enough for Magento, which has far more files than a default configuration expects.
In production, opcache.validate_timestamps should be off, so PHP does not check every file for changes on every request. That is safe because production code only changes on deploy, when the cache is cleared anyway, and it removes a filesystem check from the hot path.
Redis for the default cache
Above OPcache, Magento's application cache holds configuration, layout, block HTML, and more. This should live in Redis or Valkey, not on the filesystem.
File-based caching does not scale across multiple web nodes and adds disk pressure. Redis keeps the cache fast, shared, and in memory, which is what a store of any size needs.
The configuration lives in app/etc/env.php, pointing the default cache at a Redis instance. This is the backbone of Magento's application caching.
Redis for sessions, kept separate
Sessions also belong in Redis, and importantly, in their own separate database or instance from the cache. Mixing sessions and cache in one Redis database means a cache flush can disturb sessions and the two compete for the same memory.
Keeping sessions on a separate Redis database isolates them. A cache clear no longer risks logging customers out, and each can be sized and monitored on its own.
This separation is a common gap. A store that put sessions and cache in the same Redis database works, until a flush or a memory limit causes the two to interfere.
Full-page cache: Varnish over the built-in
The top layer is the full-page cache, which stores entire rendered pages. Magento can use its built-in PHP full-page cache or Varnish, and on any store of size Varnish is the right choice.
The built-in cache still runs PHP to serve a cached page. Varnish sits in front of the application and serves cached pages without touching PHP or Magento at all, which is dramatically faster.
The store tells Magento which to use through the full-page cache configuration, where a value of 2 selects Varnish. A production store of size returning the built-in option is leaving its biggest performance layer on the table.
How Varnish invalidates: cache tags
Varnish and Magento coordinate through cache tags. Magento tells Varnish which entities each cached page depends on, and when an entity changes, Magento instructs Varnish to purge the pages carrying that tag.
This is what keeps a full-page cache correct. A product save purges exactly the cached pages that show that product, so customers never see stale content while most of the cache stays warm.
When this coordination is misconfigured, you get either stale pages or over-broad purging. Getting the tag-based purging right is what makes Varnish safe to run in front of a live store.
Each layer exists to stop a request before it reaches the more expensive layer below.
How the layers work together on a hit
The layers form a ladder, and each exists to stop a request before it reaches the more expensive layer below. On a cached page, Varnish answers immediately, and PHP, Redis, and the database are never touched.
That is the whole point of the stack. The fastest request is the one Varnish serves without waking the application at all, and a large share of storefront traffic should be exactly that.
When Varnish is working well, the expensive layers only run for the requests that genuinely need them: logged-in actions, checkout, and the first render of an uncached page.
What happens on a miss
A cache miss runs the full stack. Varnish passes the request to PHP, OPcache serves the compiled code, the application reads from Redis where it can, and falls through to the database for anything not cached.
This is why every layer matters. A miss at the top means the layers below have to be fast, and a store with weak lower layers turns every Varnish miss into a slow request.
It also explains why a low full-page cache hit rate is so costly. Each miss pays the full price of the entire stack, which the cache existed to avoid.
Redis memory and eviction
A Redis cache is only as good as the memory it has to work with. When Redis hits its maxmemory limit, it evicts keys according to its eviction policy, and evicted cache is a guaranteed future miss.
Watch the eviction counter in Redis. A steadily climbing eviction count means the cache is too small for what the store asks it to hold, and the fix is more memory or a policy that matches the workload, not more application code.
The config cache bites during deploys
Magento's configuration cache holds merged config, and it is one of the caches most tied to deploys. A stale config cache after a deploy can make the store behave as though a change did not take effect.
This is why clearing the right caches is part of a deploy, and why the configuration cache in particular has to be fresh after config changes. A setting that "did not work" is often a config cache that was never cleared.
Understanding which cache holds what makes deploy-time cache handling deliberate rather than a blanket flush of everything. Clearing precisely is faster and avoids the cold-cache cost of a full flush.
Private content keeps pages cacheable
Personalized content is where full-page caching gets tricky, and Magento solves it with private content. Instead of making a page uncacheable because it shows the cart or the customer name, those parts load separately on the client.
The page itself stays cached and shared, while the private sections are fetched per customer through the customer-data mechanism. This is what lets a store cache category and product pages fully while still showing a personalized header.
When personalization is done by disabling the cache instead, the full-page cache stops working for those pages. Private content is the pattern that keeps the top layer of the stack effective on personalized stores.
A CDN in front of Varnish
Many stores put a content delivery network in front of Varnish. The CDN caches static assets close to users and can cache full pages at the edge, taking load off Varnish and the origin entirely.
This adds another layer to the stack, and it has to coordinate with Varnish's invalidation so the edge does not serve stale pages. Done well, it means a large share of traffic never reaches the origin data center at all.
The CDN does not replace Varnish; it sits above it. Static assets and cacheable pages are served at the edge, while Varnish and the application handle everything dynamic.
Valkey as a Redis alternative
Valkey is a fork of Redis that emerged after Redis changed its license, and it is a drop-in replacement for Magento's purposes. Where this article says Redis, Valkey works the same way.
The choice between them is mostly about licensing and support preferences, not functionality. Magento's cache and session handling treat them identically, and the configuration is the same.
The important decision is not Redis versus Valkey. It is using one of them for cache and sessions instead of the filesystem or the database.
Separate roles, separate databases
A recurring theme across the stack is separation. The default cache, the page cache, and sessions should each have their own Redis database, not share one.
Sharing them causes exactly the interference the layers are meant to avoid. A page-cache flush should not touch the application cache, and neither should touch sessions, which only holds true when they are separated.
This separation also makes each role measurable. You can watch the hit rate, memory use, and eviction of each cache independently, which is impossible when they share one database.
The block HTML cache in the middle
Between the full-page cache and the raw application sits the block HTML cache. It stores the rendered output of individual blocks, so that even on an uncached page, the pieces of it do not all have to render from scratch.
This layer matters most for pages that Varnish cannot fully cache, like logged-in views. The block cache lets those pages reuse rendered fragments instead of rebuilding every block on every request.
It lives in Redis with the rest of the application cache, and it benefits from the same separation and sizing. A healthy block cache softens the cost of the pages that miss the full-page cache.
OPcache preloading
PHP 7.4 added OPcache preloading, which can load a set of files into memory once at startup so every request has them ready. In principle this speeds up a large application by keeping core code permanently compiled and resident.
With Magento the benefit varies, and preloading needs testing rather than blind adoption. It interacts with how the codebase is structured and deployed, so it is a tune-and-measure change, not a guaranteed win.
Treat it as an advanced option once the fundamentals are solid. A correctly sized OPcache with timestamp validation off delivers most of the benefit before preloading enters the picture.
Warm the cache after a flush
Every layer that gets flushed is cold until it refills, and a cold cache is slow. After a deploy or a broad flush, the first requests pay the full cost of rebuilding the pages the cache used to hold.
Warming the cache deliberately, by crawling key pages after a flush, moves that cost off real customers. A crawler fetches the important pages so Varnish and the block cache are warm before traffic arrives.
Without warming, the warm-up cost lands on whoever browses first after a deploy. On a busy store, that is a real number of customers hitting a cold stack.
Common misconfigurations
A few mistakes show up repeatedly across the stack. Each one quietly undoes part of the caching it was meant to provide:
- OPcache left at default size, so it evicts and recompiles under a codebase it cannot fully hold.
- The full-page cache set to the built-in option instead of Varnish on a store large enough to need it.
- Sessions and cache sharing one Redis database, so a flush disturbs logins.
- Caches technically enabled but running a low hit rate because of non-cacheable blocks or eviction.
None of these produces an obvious error. They just make the store slower than its hardware should allow, which is why measuring each layer matters.
Measuring each layer
Each layer exposes its own health. OPcache has a status view showing memory use and hit rate, Redis reports hits, misses, and evictions through its info command, and Varnish reports hits and misses through varnishstat.
Reading all three tells you where the stack is working and where it is not. A high OPcache hit rate, a high Redis hit rate with low evictions, and a high Varnish hit rate together describe a healthy stack.
A weak number at any layer points at the fix. Measuring is what turns "the store feels slow" into "the full-page cache hit rate is 40 percent and here is why."
Each caching layer reports its own hit rate; a healthy stack shows strong numbers at all three.
Make full flush the last resort
Across the whole stack, one habit protects performance: prefer targeted clearing over full flushes. Clearing only what changed keeps the rest of the stack warm, while a full flush cold-starts everything at once.
On a busy store, a habit of flushing the entire cache for small changes is a recurring self-inflicted slowdown. Each flush empties Varnish, the block cache, and the application cache, and the store rebuilds all of it under live traffic.
Reserve the full flush for when it is genuinely needed, such as after a deploy. For everything else, clear precisely and let the stack keep serving from the layers that did not change.
The order to set it up
The layers build from the bottom. OPcache first, because it helps every request; then Redis for cache and sessions, because it backs the application; then Varnish for the full-page cache, because it sits in front of everything.
Setting them up in that order means each layer rests on a solid one below it. Varnish serving pages that fall through to a slow, uncached application is only half a solution.
Done in order, the stack compounds. A fast lower layer makes every miss at the top cheaper, and a strong top layer means the lower layers run far less often.
The whole stack, working together
Magento's performance is the product of its caching layers, not any single one. OPcache, Redis or Valkey, and Varnish each do a distinct job, and a gap in any of them shows up as a store slower than its hardware should be.
Knowing that every layer is configured correctly, separated properly, and running a healthy hit rate is what a full caching review confirms. Getting the whole stack right is the difference between fast hardware and a fast store.