Caches That Survive Change: Warming, CDC Invalidation and Partitioning Apart From the Database
Resizing a cache, replacing a node or failing over a region causes cold starts. How warming, log-driven invalidation and separate partitioning prevent them.
On this page
The usual cache lesson is “it makes reads fast”. The operational lesson is the reverse: a cache you cannot resize, replace or fail over without a latency spike is a liability. The database is sized assuming the cache absorbs most reads, so a cold cache is a database outage waiting for a trigger.
Why change is dangerous
| Event | What happens |
|---|---|
| New empty replica or node added | Misses on every key it owns until it fills |
| Node replaced after hardware failure | Hit rate drops, the database sees the traffic that node used to absorb |
| Region failover | The surviving region’s cache has not seen the other region’s keys, so misses flood the database |
| Cache cluster lost | All its keys miss at once |
The naive growth plan is to build a bigger empty copy, write to both, wait for old entries to expire, then switch. It works for short-lived data and fails for entries that never expire or rarely change, since they never repopulate. It also means paying for two systems during the wait.
Technique 1: warm from a healthy copy
Instead of waiting for traffic to refill the cache, copy the data across. A well-designed warmer has three roles, loosely coupled.
- Controller: orchestrate
Picks the source replica (typically the one with the most data), creates a queue for communication, starts the dump on source nodes, launches the populators, watches progress and cleans up afterward.
- Dumper: extract
Runs next to each cache node. Lists keys (memcached’s LRU crawler can enumerate them), saves keys and values into chunk files, uploads each chunk to object storage when it reaches a size, and posts a message with the chunk’s location.
- Populator: fill
Pulls messages from the queue, downloads each chunk, and inserts into the new nodes. It starts as soon as the first chunk exists rather than waiting for the dump to end, and can scale with the backlog.
Two design choices carry the safety:
Warming a single node is the same pipeline with a filter. When a node restarts empty and signals the controller, the controller does not use that node’s own replica as the source, because its peers kept receiving writes while it was down. It dumps from other replicas, and only for keys that hash to the node being warmed, which makes the job small and fast. Netflix has reported replacing a node and refilling it in under fifteen minutes this way, and warming new replicas holding about twelve terabytes in roughly two hours.
The order matters: new capacity should receive its data first, then live reads. Treat “warm” as a gate in the deploy, with a hit-rate threshold, not as something that happens eventually.
Technique 2: invalidate from the change log
The classic cache bug is a write path that forgets to clear a key. Every new service, script or manual fix is another chance. The robust approach moves invalidation out of application code.
write -> database -> change log (binlog / WAL)
|
CDC service tails it
|
delete or refresh matching cache keys
An integrated cache works like this: the query layer, not each microservice, reads from the cache on a hit and falls through to storage on a miss. A separate change-data-capture service tails the database’s log and invalidates the matching entries. Every writer is covered, because the database log sees every write.
Caveats to design for:
- Lag. Invalidation arrives after the commit, so a reader can briefly see an old value. Set a TTL as a backstop and decide which reads must bypass the cache.
- Ordering. Apply changes in log order, and prefer delete-then-refill over writing values from the log, which risks storing an older version.
- Races. A reader who missed just before a write can repopulate the cache with the old value after the invalidation. Version your entries or use short TTLs on misses.
Technique 3: warm across regions
A region failover sends traffic to a cache that has never seen those keys. One mitigation is to replicate keys continuously: tail the cache’s write stream in one region, and in the other region have a consumer read each key through the normal query path, which fills the local cache from the local database. You pay a little extra read traffic all the time to avoid a cliff during failover.
Technique 4: partition the cache differently
Suppose cache and database are sharded identically, so cache cluster 3 fronts database shard 3. If cluster 3 goes down, every miss lands on shard 3, which was sized for a cache-protected load. One cache failure becomes one database hot spot.
Partition the cache with a different scheme and each cache cluster holds keys from many database shards. Losing a cluster spreads its misses thinly across all shards, each absorbing a small increase.
same scheme: cache C3 down -> 100% of its misses -> DB shard 3 (overload)
different scheme: cache C3 down -> 1/N of misses -> each shard (absorbed)
Checklist
Do
Warm new capacity before it takes reads, use add-only inserts, invalidate from the change log, keep TTLs as a safety net, and partition cache and database independently. Rehearse a node replacement and a region failover and watch the database.
Don't
Size the database for the cached load and assume the cache never goes away. The first time it does, you find out how much of your capacity was borrowed from it.
Takeaway
A cache is only as good as its worst day: resize, replacement, failover and invalidation. Warm from healthy replicas with a decoupled pipeline and add-only writes, let the database log drive invalidation, replicate hot keys across regions, and shard the cache independently from the data. Then the cache is something you can operate, not just something you depend on.
Frequently asked questions
What is a cold cache problem?
A cache that is empty or missing the hot data sends every request to the database until it refills. When a node is replaced, a cache grows or a region fails over, hit rate collapses, latency spikes and the database can be overwhelmed by misses at exactly the wrong moment.
How does change data capture keep a cache correct?
A service reads the database's change log and invalidates or updates the matching cache entries whenever a row changes. Because it follows the database's own record of writes, it catches changes from every writer, not just the application code that remembered to clear the cache.
Why use add instead of set when warming a cache?
An add only writes if the key is absent. A warming process copying a snapshot could otherwise overwrite a newer value that live traffic wrote moments earlier. Add-only inserts let live writes win, so warming can never make the cache staler.
Why partition the cache differently from the database?
If cache and database shard identically, losing one cache cluster sends all of its misses to the same database shard, creating a hot spot. A different partitioning scheme spreads one cache cluster's misses across many database shards, so the failure is absorbed, not concentrated.
/* Comments */
Comments are offline right now — we reconnect automatically, nothing is lost.