The Boring Cache Stack: Redis + Varnish Tuning for Magento That Pays Rent
A small shop's server was spending 24 GB of RAM caching the same pages twice. Here's the diagnosis, the fix, and the config philosophy that keeps it fixed.
This one starts with a real graph. A small Magento 2.4.5 shop — a few thousand SKUs, healthy but modest traffic — running on self-hosted infrastructure. Redis: 12 GB and climbing. Varnish: also about 12 GB. On a host where RAM is literally the monthly bill, half the machine was gone before PHP served a single uncached request.
The instinct is to buy a bigger server. The correct move is to ask the question that unravels the whole thing: why do two caches have the same appetite?
The diagnosis: Magento was caching every page twice
Magento’s full page cache (FPC) has a setting most installations never touch: system/full_page_cache/caching_application. It has two values — built-in FPC, or Varnish. And here’s the trap: if nobody ever sets it explicitly, it defaults to built-in.
So this shop had Varnish dutifully sitting in front, caching every rendered page — while Magento, unaware, also wrote every rendered page into Redis. Two complete copies of the page cache, on the same host, each convinced it was the one doing the job. Twelve gigabytes, twice.
The fix is one command:
bin/magento config:set system/full_page_cache/caching_application 2
(2 means Varnish. Then flush, and watch Redis deflate.)
If you run Magento behind Varnish and have never explicitly set this value, stop reading and go check. This is the highest-value single command in the article.
While you’re in there, a related trap from the same shop: use_stale_cache had been enabled on a stack of cache types, which balloons shared memory for a benefit that only exists when Redis is far away. If your Redis is on the same host or a container one bridge-hop away, stale cache is solving a latency problem you don’t have — leave it off.
The principle: every cache layer gets exactly one owner
The double-caching bug is a symptom of a missing rule. State it once and the rest of the stack designs itself:
- Varnish owns the full page cache. Rendered HTML, cached at the edge, never touching PHP or Redis.
- One Redis owns the application cache. Layout, block HTML, configuration — the regeneratable stuff Magento needs between requests.
- A separate Redis owns sessions. Not a separate database on the same instance — a separate instance.
That last separation earns its keep three ways: flushing the cache can never log your customers out, memory pressure on one can’t evict the other, and — the part that justifies this article — you can give each one the configuration it actually deserves. Because here’s the thing almost every tuning guide misses:
Cache Redis and session Redis want opposite configurations
Cache entries are disposable. Magento can regenerate any of them. Losing one costs a few milliseconds of recomputation; keeping stale ones costs correctness. So the cache Redis should be fast, capped, ruthless, and amnesiac:
# cache Redis — the disposable one
maxmemory 4gb
maxmemory-policy allkeys-lru
# persistence: fully off — a cache that survives restarts
# is a liability (stale data on a cold start)
save ""
appendonly no
lazyfree-lazy-eviction yes
lazyfree-lazy-expire yes
allkeys-lru evicts the least-recently-used key regardless of TTL — correct, because any cache entry is safe to lose. Persistence off — correct, because rebuilding from empty is the safe cold-start, and you skip the disk I/O entirely. And maxmemory is the line that fixes the original incident’s other half: nothing in Redis stops growth by default. The 12 GB happened because no cap existed. With Varnish owning FPC, 2–4 GB is a generous ceiling for a small or medium shop’s application cache.
Sessions are precious. A session is a logged-in customer and a shopping cart. It is not regeneratable — losing it doesn’t cost milliseconds, it costs a checkout. So the session Redis inverts every choice:
# session Redis — the precious one
maxmemory 1gb
maxmemory-policy volatile-lru
# persistence: lightweight RDB snapshots, so a restart
# doesn't log everyone out and empty their carts
save 900 1
save 300 10
save 60 10000
appendonly no
volatile-lru only evicts keys that carry a TTL — Magento sets one on every session, so in practice it behaves like LRU, but it can never touch a key without an expiry (locks, internal markers). A protective default for data you can’t rebuild. RDB snapshots are the pragmatic middle: full AOF durability is overkill for sessions, but snapshots mean a container restart costs at worst a few minutes of session updates instead of every cart in the shop. On a session-sized dataset the disk I/O is a rounding error.
Two instances, opposite eviction policies, opposite persistence, different passwords, and suddenly every config line has a reason instead of being pasted from a gist. That’s the whole philosophy of this stack: not clever, just deliberate.
The env.php side
Three details worth getting right in Magento’s own config once Varnish owns FPC:
Point the two cache frontends at their databases — default at db 0, page_cache at db 1 — and set compress_data to 1 on default but 0 on page_cache. Once Varnish does the heavy FPC lifting, whatever still lands in page_cache isn’t worth spending CPU compressing.
Session locking stays on (disable_locking => '0'). For e-commerce this is non-negotiable — concurrent AJAX requests racing to write the same cart is exactly the bug you’ll never reproduce and never stop hearing about. If you ever see “unable to acquire session lock” under load, the fix is finding the slow code path holding the lock, not loosening the locking.
Auth is a decision, not an accident. On a private container network, running Redis auth-less is a defensible trade-off for a small shop — but make it deliberately, and differently per instance if you do use passwords. “I never thought about it” is the only wrong configuration.
Prove it worked
Regular readers know where this goes: a change without evidence is a hope. Four checks, five minutes:
# 1. Redis has stopped hoarding — used_memory should sit
# comfortably under maxmemory with evicted_keys near zero
redis-cli -h redis-cache INFO memory | grep -E "used_memory_human|maxmemory_human"
redis-cli -h redis-cache INFO stats | grep -E "evicted_keys|keyspace"
# 2. FPC keys are gone from Redis — db 1 should be near-empty
redis-cli -h redis-cache -n 1 DBSIZE
# 3. Varnish is actually the one answering
varnishstat -1 -f MAIN.cache_hit -f MAIN.cache_miss
# 4. And the customer-visible truth: response headers
curl -sI https://shop.example/ | grep -iE "x-magento-cache|age"
A healthy end state on this class of shop: cache Redis idling in the low gigabytes with a hard ceiling it never hits, session Redis in the hundreds of megabytes, Varnish hit rate doing the real work, and several gigabytes of RAM handed back to the things that earn revenue — PHP workers and MariaDB. On a small host, that reclaimed RAM is the difference between “we need to upgrade the server” and “no, we don’t.” Hence the title: this tuning pays rent.
The habit, restated
Nothing in this article is advanced. That’s the point. The 24 GB incident wasn’t caused by a hard problem — it was caused by a default nobody checked, a cap nobody set, and two data types sharing one configuration when they wanted opposites. Boring, deliberate, evidenced: give every cache one owner, give every Redis a reason for every line, and check the graph afterward.
Madalin
AI integrator🚀 Senior Architect | SRE & Database Expert | AI Orchestrator 👋 Building the future at the speed of thought. ⚡️ I don't just write code; I architect high-performance, bulletproof ecosystems. With a foundation in Systems Engineering and a mastery of Go and TypeScript, I bridge the gap between heavy-duty backend reliability and seamless, high-conversion frontends.
Continue the conversation
If this article reflects the challenges your organisation is navigating, explore more practical guidance across Madalin.