Skip to content

The caching layers

2 min read · main part only; folded sections are optional

After this you can answer: Which caches sit between a user and Postgres, and how does each one get cleared?

flowchart TB
B["browser<br/>React Query, 30 s"] --> W["web data cache<br/>30 s, tag events"]
W --> V["Valkey events cache<br/>soft 30 s, hard 300 s"]
V --> PG[(Postgres)]
API["API, after a change commits"]
API -.->|"2. POST /api/revalidate"| W
API -.->|"1. bump generation"| V
  1. Cache-aside. The API looks in Valkey first. On a miss it reads Postgres and stores the copy. The log line says fresh, stale, miss or error.
  2. Soft and hard age. After about 30 s (plus random jitter) a copy is stale: one request rebuilds it under a 5 s lock while the others get the stale copy. After 300 s it is deleted.
  3. Clear by generation. A change bumps one counter, events:upcoming:gen. Every key contains the generation, so all old keys are ignored at once and expire by themselves.
  4. Clear the web cache. The API then calls the web server’s POST /api/revalidate with the shared secret, which drops every list tagged events.

The Valkey side is apps/api/src/queries/events-cache.ts → getUpcomingFromCache, driven by apps/api/src/services/events.ts → listUpcomingEvents. The web side is apps/web/src/features/events/server/cached-events.ts.

Each Valkey call gives up after 100 ms, so a slow Valkey falls through to Postgres instead of slowing every page. If Postgres fails, an old copy is served (stale-if-error).

Measured locally: with the revalidate call, a new event was in the server HTML after 8-68 ms; without it, after about 30 s. A change nothing clears took 90.6 s, because each layer can refresh from a stale copy below it. HTML pages and API answers send no-store, so no shared cache keeps them.

From → toWhat happensWhat allows it
API app → Events list cacheevents.list asks the cache first
Events list cache → ElastiCache Valkeyonly when dataEnabled = truethe cached lists and the generation key
See the events cache on the map →

The cache is keys in Valkey, not a console page. The API log’s request lines carry the cache field.

SymptomCauseWhere to look
A new event shows only after ~30 sThe revalidate call failedAPI log: revalidate warnings
Many cache error lines, Postgres load risesValkey slower than 100 msElastiCache metrics
Web log: 401 on /api/revalidateThe two tasks hold different secretsRestart both services
What is a cache stampede, and how is it stopped?

When a popular entry expires, every request misses at once and hits Postgres. Measured before the fix: 100 requests made 100 database queries; after it, 1. Only the lock holder rebuilds; the others serve the stale copy.

Why bump a generation instead of deleting the key?

There is one key per filter. Deleting means finding them all. One INCR moves every filter to a new, empty key at once, and a reader that started before the change can never write old data back under the new generation.

Why commit first and invalidate second?

The other order leaves a gap: a reader could refill the cache from the old data before the commit. Write, then invalidate, so the next miss reads the new data.

What is the trade-off of clearing on every booking?

Every booking empties every cached list. With many bookings per second almost every read would be a miss. Fine at our traffic; later, seat counts could be cached apart from the list.

Advanced: Keys and versions

A key looks like events:upcoming:v3:g7:<16-hex hash of the filters>. v3 is the shape version: a deploy that changes the cached value bumps it, so new code never reads old-shaped data. The hash keeps keys short and free of special characters. Search results are not cached in these layers; each search text would make its own key.

Answer these without scrolling up. If one feels hard, reread that section.

  1. Name the three caches in front of Postgres for the events list.
  2. What is the difference between the soft and the hard age?
  3. How does one write clear every filter’s key at once?
  4. What happens when Valkey answers slower than 100 ms?
  5. Why can a change that nothing clears take longer than 30 + 30 seconds?

Redraw it from memory: browser → web data cache → Valkey → Postgres, and the two arrows that clear them

Try this: on the map, play the sale-day flow and open the failure branch of the events cache step

Read this book online: https://book.theversehub.in/