Caching Basics for Backend Apps
Storing the result of expensive work so the next request doesn't have to redo it — and the hard part, invalidation.
3 min read
Caching stores the result of an expensive operation — a database query, a call to a third-party API, a computed value — so that a later request asking for the same thing gets an instant answer instead of redoing the work. It's one of the most effective performance tools a backend has, and also one of the easiest to get subtly wrong.
Where caching happens in a backend
In-memory / application cache — data cached directly in the running process's memory. Fastest possible access, but not shared across multiple server instances and lost on restart.
Distributed cache (Redis, Memcached) — a shared cache all server instances can read and write, so a value computed by one instance is available to all of them.
// pseudocode
function getUserProfile(id):
cached = redis.get(f"user:{id}")
if cached:
return cached
user = database.query("SELECT * FROM users WHERE id = $1", [id])
redis.set(f"user:{id}", user, ttl=300) // cache for 5 minutes
return user
HTTP caching — using standard headers (Cache-Control, ETag) so browsers, proxies, and CDNs can cache a response without even reaching the backend for repeat requests.
HTTP/1.1 200 OK
Cache-Control: public, max-age=300
ETag: "a1b2c3"CDN caching — caching entire responses at edge locations physically close to users, mainly for content that's the same for everyone (public API responses, static assets) rather than per-user data.
The hard part: invalidation
The classic line is: "There are only two hard things in computer science: cache invalidation and naming things." Caching is easy to add; caching correctly is hard, because a cached value can go stale the moment the underlying data changes, and a stale response served with confidence is worse than a slow one — the app looks correct while actually lying.
Two common strategies:
- Time-based expiry (TTL) — cache a value for a fixed duration, accepting that it may be up to that long out of date. Simple, and good enough for data that doesn't need to be instantly fresh (a homepage list of popular articles).
- Explicit invalidation on write — when data changes, actively delete or update the cached value at the same time, so reads are never stale for longer than the write takes. More correct, but requires remembering to invalidate every code path that writes the data — miss one, and you get a stale cache with no TTL to eventually fix it.
Most real systems combine both: explicit invalidation for the writes you remember to handle, and a TTL as a safety net for the ones you don't.
What's worth caching
Good candidates: expensive queries or computations that are read far more often than they change, results from slow third-party APIs, and data that's fine being slightly stale (a few seconds to minutes). Poor candidates: data that must always be perfectly current (an account balance right before a transfer), or data that's cheap to compute and rarely re-read — the cache overhead isn't worth it.
Caching reduces how often a backend has to do expensive work at all. The next lesson turns to what happens when things go wrong anyway — how a well-built backend handles errors instead of just crashing.