An outage can be slower than a miss
The bypass path here takes 198 ms against 150 ms for a miss, because the application waits out a timeout before giving up. Short timeouts and circuit breakers exist for this reason.
Because the application may never have to reach the database. Redis keeps a copy of the answer in memory, and the speed comes with new ways to be wrong.
All timings on this page are illustrative. They show the shape of the difference, not what your system will do.
The cache starts empty, so the first request takes the long way round and leaves a copy behind.
Ready. Send a request and watch the path it takes.
No requests yet. Each one is drawn here to scale.
(empty keyspace)
| id | name | plan |
|---|---|---|
| 42 | Asha | free |
| 7 | Bikram | pro |
Live: the line running nowcache-aside.js
async function getProfile(id) { const key = `user:${id}`; try { const cached = await redis.get(key); if (cached) return JSON.parse(cached); } catch (err) { if (STRICT) throw err; // otherwise carry on without the cache } const row = await db.getUser(id); redis.set(key, JSON.stringify(row), { EX: TTL }).catch(log); return row;} async function updateProfile(id, patch) { await db.updateUser(id, patch); await redis.del(`user:${id}`);}Redis doesn't sit in front of Postgres, and it has never heard of your database. This pattern is called cache-aside: the application reads the cache, falls through on a miss, and writes the result back. Every behaviour above is a line in that function.
Note that an update deletes the key rather than overwriting it. Two concurrent writers can apply their cache writes in the wrong order and leave an old value behind for a full TTL. Deleting narrows that window, and the next read repopulates from the truth.
On the wire
A cache stampede happens when a popular key expires and every waiting request misses together. Postgres suddenly sees the traffic the cache was absorbing. Run it both ways and watch the query counter on the database.
Common defences are a short lock so one request rebuilds the entry, adding random jitter to TTLs so keys don't expire together, and refreshing popular keys before they expire.
The bypass path here takes 198 ms against 150 ms for a miss, because the application waits out a timeout before giving up. Short timeouts and circuit breakers exist for this reason.
Without invalidation, a value can be wrong for as long as its TTL. A short TTL shrinks that window and costs more misses. There's no setting that gives you neither.
If the cache was absorbing most reads, “fall back to the database” can turn a Redis outage into a database outage. Failing fast is sometimes the safer choice.
user:42 was cached with a 30 s TTL. 31 seconds later, the same key is requested. What happens?
The struck-through row shows this. Redis removes expired keys when they're accessed, and with a background sampler. Either way an expired key is never returned to a client, so the logical behaviour is exact even though memory is reclaimed later.
The simulated restart loses everything, as a pure cache with persistence turned off would. With RDB snapshots or an AOF, keys can return after a restart, including ones whose invalidation failed during the outage.
This page covers its most common role. It also offers lists, sets, sorted sets, streams and pub/sub. Replication, clustering and eviction under memory pressure (maxmemory-policy) are all out of scope here.
They were picked to give 5 ms and 150 ms. Real numbers depend on network distance, payload size, query plans and load. A well-indexed Postgres read can be very fast. What holds up is the shape: a network round trip to memory usually costs far less than a query that has to do real work.