NextdoorRonak ShahProblem & solution
Scaling Nextdoor’s Datastores: Part 4
Summary
Look-aside caching systems can become inconsistent when concurrent database updates execute cache writes out of order, allowing stale data to overwrite newer modifications. To prevent these racing writes, Nextdoor introduced a unique, monotonic db_version column to Postgres tables using database triggers that initialize version numbers on insert and increment them on update. Application updates retrieve this new version inside a transaction block and attach it as a metadata header to serialized cache values. Redis then executes custom Lua scripts, specifically set_if_version and del_if_version, to perform atomic conditional updates that reject any incoming payload with a version lower than or equal to the stored version. This serializable check ensures that out-of-order writes are dropped and the cache remains strictly aligned with the latest database state.
Context
In distributed look-aside caching systems, concurrent web requests can execute database updates and subsequent cache writes in different orders. When an earlier database write updates the cache after a later write has already finished, stale data overwrites fresh data, causing the cache and database to become inconsistent.
Approach / What changed
The team added a unique, monotonic db_version column to database tables, populated via a Postgres BEFORE INSERT OR UPDATE trigger. Because Django's ORM does not return updated rows, the application executes a select within the same transaction to obtain the new version. The version is attached as a metadata header on the serialized cache object. On the cache layer, Redis executes custom Lua scripts (set_if_version and del_if_version) to perform atomic compare-and-set operations, rejecting updates when the incoming version is equal to or lower than the stored version.
Takeaways
Timestamp-based versioning is insufficient for cache consistency because timestamps are neither guaranteed to be strictly monotonic nor globally unique across concurrent writers.
Row version numbers must be generated atomically on the database side via mechanisms like database triggers or user-defined functions rather than client-side logic to ensure monotonic consistency.
Running custom Lua scripts inside single-threaded Redis enables atomic conditional updates and deletes, preventing race conditions that occur when version checks happen on the client side.
Source and freshness
- Source
- engblog.nextdoor.com
- Published
- Added to Yomu
- Last updated
Summaries are generated from the original by a language model. Check the original before relying on it.
Keep reading
Related reading
The Good Stuff
Paid picks we use ourselves. Links carry a referral code — a subscription through one earns us a commission at no extra cost to you.
The best engineering reads, once a week
One useful email. Unsubscribe anytime.