Pakkit.net
← Back to blog

Engineering Practice

Decide If Your Cache Is Allowed to Lose Data

A store like Redis can be a disposable accelerator or a durable database, and the same server behaves completely differently depending on which you chose — so make the call on purpose, because the expensive bugs come from treating it as one while it's configured as the other.

  • Engineering Practice
  • Databases
  • Architecture
  • Reliability

An in-memory store like Redis is unusually good at hiding a decision you should be making explicitly. Out of the box it’s fast, it holds your data, and it just works — so it’s easy to lean on without ever answering the question that actually matters: is this thing allowed to lose what I put in it? Because the same server can be a disposable cache that drops data whenever it’s convenient, or a durable store that treats every write as sacred, and those are opposite contracts wearing the same interface. The bugs come from assuming one while you’ve configured the other.

The same server, two opposite contracts

Configure it as a cache and the deal is: data here is disposable. When memory fills, it evicts whatever it decides is least useful. It might not survive a restart. That’s not a flaw — it’s the whole point of a cache, which is a fast copy of truth that lives somewhere else. You get speed by giving up durability, on purpose.

Configure it as a durable store and the deal flips: writes are persisted to disk, nothing gets evicted out from under you, and a restart comes back with your data intact. Now it’s a database that happens to be fast, and you’re paying for durability on every write.

The dangerous part isn’t picking wrong. It’s not realizing there was a pick — and building on a contract you never actually chose.

The failure is a mismatch between what you assume and what you set

Almost every painful Redis story is the same shape: the code assumes one contract and the configuration provides the other.

  • Trusting a cache to be durable. You store the only copy of something in there — a session, a job’s state, a counter that matters — and treat it as safe. Then memory pressure evicts it, or a restart clears it, and the data is gone, with no source to rebuild from. It worked in testing because you never hit the eviction path. It fails in production because you did.
  • Paying database prices for a cache. The reverse: you turn on full persistence and disable eviction for something that was only ever a disposable accelerator. Now you’re taking write latency and memory-exhaustion risk for data you’d have happily recomputed. Worse, with eviction off, a store that fills up stops accepting writes instead of shedding load — a cache that can wedge itself.

Neither bug is exotic. Both come from skipping the one question at the start.

Make the decision, then let it drive the config

So decide it explicitly, up front: is this data’s home here, or does its truth live somewhere else? That single answer drives everything else:

  • If it’s a cache (truth lives elsewhere): eviction on with a sensible policy, persistence optional or off, and — critically — application code that treats every read as a possible miss and can rebuild from the source of truth. A cache your app can’t tolerate losing isn’t a cache; it’s a single point of failure with a fast interface.
  • If it’s the system of record (truth lives here): persistence on, eviction of live data off, memory sized for the real working set, and backups — because now losing it is losing data, not losing a copy.

The point isn’t which one is correct. It’s that the two configurations are answers to two different questions, and you have to know which question you’re answering.

Know how long the data is supposed to live

Underneath all of this is a question of lifetime, which is a thing worth being explicit about for any piece of state, not just Redis — the theme of know how long your state lives. A value that should vanish after an hour, a value that must survive a reboot, and a value that’s fine to lose on eviction are three different lifetimes, and an in-memory store lets you assign any of them — including, by accident, the wrong one. Naming the intended lifetime is how you catch “I’m treating a five-minute cache entry like a permanent record” before it bites.

And when something built on a cache misbehaves in that maddening “I fixed it but it’s still wrong” way, the cache’s contract is the first place I look — because a stale or evicted value serving old truth is exactly the shape of when in doubt, suspect the cache.

Ask the question out loud

The habit is small: before I put anything in a fast in-memory store, I say out loud whether it’s allowed to disappear. “Yes, it’s a cache, the real copy is in the database” — then I code for misses and turn on eviction. “No, this is the only copy” — then I turn on persistence and treat it like the database it now is. The store won’t ask you the question. It’ll happily be either thing. Which means it’s on you to decide, on purpose, whether it’s allowed to lose your data — before production decides for you. If you’ve been burned by a cache you assumed was durable, I’d like to hear how it went.