← Two Hard Things: caching bugs

Two Hard Things: caching bugs · Part 1

You added a cache. Now you have two databases.

· by Tony Tang

Updating the cache on every write races: two writes land in Redis in the wrong order and the old email wins. The boring fix: on write, delete the key. The next read fetches the truth and caches that. Keep the TTL anyway, as a safety net for the invalidation you forgot.

Comment below: What's the longest TTL in your app?

Two Hard Things: a series on the caching bugs you already ship, and how to fix them. "There are only two hard things in computer science: cache invalidation and naming things." (Phil Karlton) All code is real TypeScript on Node, typechecked before it went on screen, and every claim was reproduced against real Redis and Postgres.

Transcript

You added a cache. Congrats. You now have two databases. Get user checks Redis first. Miss? Read Postgres. Cache it for an hour. Fast. Then a user changes their email. Postgres has the new one. Redis has the old one for the next hour. So you update the cache on every write, too. But two writes race. They land in Redis in the wrong order, and the old email wins. The fix is boring. On write, don't update the cache. Delete the key. The next read misses, fetches the truth, and caches that. Order doesn't matter anymore. A delete is a delete. And keep the TTL anyway. It's your safety net for the invalidation you forgot. Best practice, the database is the truth. The cache is a copy with an expiry date. But delete on write still has a race. A tiny one. Part two. What's the longest TTL in your app?

More from this series