Cloud Design Patterns · Part 5 of 10 · October 18, 2026 · 8 min read

Cache-Aside With Request Coalescing:
Surviving the Cold Cache

A cache your database cannot survive without is no longer an optimization. Coalesce misses, jitter expiry, and serve stale data while you refresh.

Steel shelving filled with identical gray storage boxes in a dim warehouse, one amber pendant lamp lighting a few of them.

Picture a typical retail catalog on AWS. The product pages read from an Amazon ElastiCache cluster, and the cache answers 98% of reads. The Amazon Aurora database behind it is sized for the other 2%, and on a normal weekday it idles at 15% CPU.

Then a cache node is replaced during a maintenance window. Or a deploy changes the key prefix. Or someone flushes the cluster to clear one bad entry. For the next few minutes, every product view is a miss, and a database sized for 2% of reads receives 100% of them: fifty times its normal read load, all at once, much of it for the same few hundred popular rows.

Connections run out. Queries queue. Page loads time out, the load balancer's health checks start failing on application servers that are stuck waiting on the database, and the cache stays empty because the requests that would fill it never finish.

That scenario is a composite, not one client's outage. Nothing in it is a bug in the usual sense. Every component did exactly what it was built to do. The design assumed the cache would be warm.

The Cache Became a Load-Bearing Wall

Cache-aside is the most common caching pattern on AWS, and the simplest. The application reads the cache. On a hit, it is done. On a miss, it reads the database, writes the result into the cache with a time to live (TTL), and returns it. The ElastiCache documentation calls it lazy loading.

The trouble starts with success. As the hit ratio climbs, the database looks oversized, so someone sizes it down. Now the cache is not an optimization. It is a load-bearing wall, and the system has two very different operating modes: warm, where everything is fast and cheap, and cold, where the database receives traffic it was never sized for.

Three things push a system into the cold mode:

  • A hot key expires. Thousands of concurrent requests miss the same key in the same second, and every one of them runs the same query for the same row. This is the thundering herd, and it can happen on a perfectly healthy cluster.
  • The whole cache goes cold. A node replacement without a replica, a failover to a new cluster, a flush, or a deploy that changes how keys are built.
  • TTLs line up. Items loaded together, by a warm-up job or after a restart, all expire together, so the stampede repeats on a schedule.

Facebook's outage of September 23, 2010 is the public example of a cache feedback loop. An automated system checked cached configuration values and replaced invalid ones from the database. When the database copy was itself bad, every client that got an error treated the value as invalid and deleted the cache key, which sent still more queries to databases that could not recover. Facebook stopped all traffic to the database cluster, which meant turning off the site, to break the loop.

The Amazon Builders' Library article Caching challenges and strategies covers the same ground from inside Amazon: cold starts, thundering herds, and request coalescing. The lesson is the same at any scale. Once a service depends on its cache, losing the cache is a failure mode you have to design and test for.

Coalesce, Jitter, and Serve Stale

The fix is not a bigger cache. It is a cache-aside implementation that behaves well at the moment the cache is empty. Five techniques do most of the work.

Coalesce misses for the same key. When many callers miss the same key at once, exactly one should go to the database. Inside one process, this is single-flight: the first caller starts the load and the others wait on its result. Across a fleet, it is a short lease lock in the cache itself. The caller that wins SET key:lock token NX PX 5000 loads the row and fills the cache. The callers that lose either serve a stale copy or wait a few milliseconds and read the cache again. The lease expires on its own, so a loader that crashes cannot block the key forever, and the winner releases the lock only if the stored token is still its own.

Add jitter to every TTL. Instead of a fixed 300 seconds, use 300 to 360 seconds at random. Expirations spread out, and a warm-up job no longer schedules its own stampede.

Serve stale while one caller refreshes. Store a soft expiry inside the cached value and set a longer hard TTL on the key. After the soft expiry, the first caller to notice takes the lease and refreshes; everyone else keeps getting the slightly old value. For most product, profile, and catalog data, ten seconds of staleness is invisible to users. A database outage is not.

Cache negative results briefly. A lookup for a key that does not exist misses every time, so an attacker or a bad client can walk straight through the cache. Cache "not found" for 30 to 60 seconds.

Warm before you shift traffic. A new cluster, a new key format, or a new Region should be filled before it takes production reads, by replaying recent keys or by dual-writing for a while, and then ramped up.

None of this removes the need for the database to survive some cold-cache load. Coalescing turns ten thousand queries for one key into one query, but a cold cache with a million distinct hot keys is still a million queries. Cap the database's connections, and shed what it cannot serve (post 4) instead of letting it fall over.

GET item (miss)SET lock NX PX 5000 (won)GET item (miss), SET lock (lost)stale copy, or wait brieflyone queryrowSET item, TTL plus jitterCaller ACallers B and CElastiCacheDatabase
One miss loads the database; concurrent misses wait for it or get stale data

Building It on AWS

On AWS, the lease lock lives in ElastiCache for Valkey or Redis OSS, which gives you atomic SET NX with an expiry in one command. Run the cluster with replicas in more than one Availability Zone, so a primary failure promotes a replica that already holds the data instead of starting cold. Put request collapsing at the edge with Amazon CloudFront, which already does it for you.

ServiceSettingValueSource
Amazon ElastiCache (Valkey or Redis OSS)Lease lockSET key:lock token NX PX 5000 per hot key; release only when the token matchesValkey SET, ElastiCache caching strategies
Amazon ElastiCache ServerlessCapacityScales without node sizing; Valkey 7.2 and later, Redis OSS 7.1, Memcached 1.6.22 and laterWhat is ElastiCache
Amazon ElastiCacheCacheHitRate, EvictionsThe two metrics that show a cache going cold or too smallElastiCache metrics
Amazon DynamoDB Accelerator (DAX)Item cache TTLDefault five minutes; write-through for items written through DAXDAX item cache
Amazon CloudFrontRequest collapsingSimultaneous requests with the same cache key for a missing or expired object go to the origin onceCloudFront traffic spikes
Amazon CloudFrontstale-while-revalidate, stale-if-errorServe stale content while refreshing, or when the origin returns errorsCloudFront cache expiration

Here is the core of a coalescing read in Python with redis-py. The loser of the lock waits briefly and reads again; after three tries, it gives up and lets the caller shed the request rather than join a stampede.

import random, time
from uuid import uuid4

def get_product(r, db, pid, ttl=300):
    key, lock = f"product:{pid}", f"product:{pid}:lock"
    for _ in range(3):
        if (hit := r.get(key)) is not None:
            return decode(hit)
        token = uuid4().hex
        if r.set(lock, token, nx=True, px=5000):     # one loader per key
            try:
                row = db.load_product(pid)
                r.set(key, encode(row), ex=int(ttl * random.uniform(1.0, 1.2)))
                return row
            finally:
                release(r, lock, token)              # Lua: DEL only if value == token
        time.sleep(0.05)                             # someone else is loading
    raise CacheBusy(pid)                             # shed, don't stampede

When Not to Use It

Data that must be current for every read. An account balance, or inventory at the moment of checkout, should not come from cache-aside. Read it from the database of record, and cache only the views around it that can tolerate a few seconds of age.

Low-traffic keys. Coalescing costs a lock round trip on every miss. For keys that see a request a minute, plain cache-aside with jittered TTLs is enough.

Small data that rarely changes. A list of countries, a pricing table, or a feature catalog that changes weekly does not need a distributed cache. Load it into memory at startup, or publish it as a static file through CloudFront. If many query shapes need precomputed answers, a read model (post 7) is the better tool.

How We Check for It

This sits in the Performance Efficiency pillar, under PERF03, "How do you store, manage, and access data in your workload?", and its best practice PERF03-BP05, "Implement data access patterns that utilize caching." It also sits in the Reliability pillar under REL05-BP01, "Implement graceful degradation to transform applicable hard dependencies into soft dependencies," because a cache the database cannot run without is a hard dependency.

In a Vantalect review, we look at:

  • What happened to database load the last time the cache was flushed or failed over in a test environment, and whether anyone has tried.
  • TTL settings in code, and whether they carry jitter.
  • CacheHitRate and Evictions in CloudWatch over the last month, and the database's headroom if the hit ratio fell to zero.
  • Whether the cache runs with replicas across Availability Zones, and whether a failover has been tested.
  • Whether hot keys are coalesced, or whether one popular item can send thousands of identical queries to the database.

If you want those answers for your own workloads, that is what the Well-Architected Review is for.

This post is part 5 of Cloud Design Patterns.

These patterns are what
a review checks for.

The AWS Well-Architected Framework Review measures your workload against these patterns in your live accounts, and the Remediation closes the High-Risk Issues it finds.

The next conversation

Let's define the result.

Tell Charles which challenge is in front of you. He will scope the work and make the result he will measure clear before you sign.

Book a 15-minute call with Charles

Remote engagements nationwide.
Serving clients since 2020.