The caching layers
After this you can answer: Which caches sit between a user and Postgres, and how does each one get cleared?
Picture it
Section titled “Picture it”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"| VHow it works
Section titled “How it works”- Cache-aside. The API looks in Valkey first. On a miss it reads Postgres and stores the copy.
The log line says
fresh,stale,missorerror. - 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.
- 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. - Clear the web cache. The API then calls the web server’s
POST /api/revalidatewith the shared secret, which drops every list taggedevents.
In MiniTickets
Section titled “In MiniTickets”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 → to | What happens | What allows it |
|---|---|---|
| API app → Events list cache | events.list asks the cache first |
|
| Events list cache → ElastiCache Valkeyonly when dataEnabled = true | the cached lists and the generation key |
|
See it in AWS
Section titled “See it in AWS”The cache is keys in Valkey, not a console page. The API log’s request lines carry the cache
field.
aws logs tail /minitickets/dev/api --since 10m --filter-pattern cache
The revalidate route: apps/web/src/app/api/revalidate/route.ts → POST.
Break it
Section titled “Break it”| Symptom | Cause | Where to look |
|---|---|---|
| A new event shows only after ~30 s | The revalidate call failed | API log: revalidate warnings |
Many cache error lines, Postgres load rises | Valkey slower than 100 ms | ElastiCache metrics |
Web log: 401 on /api/revalidate | The two tasks hold different secrets | Restart both services |
Say it in an interview
Section titled “Say it in an interview”Follow-ups
Section titled “Follow-ups”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.
Checkpoint
Section titled “Checkpoint”Answer these without scrolling up. If one feels hard, reread that section.
- Name the three caches in front of Postgres for the events list.
- What is the difference between the soft and the hard age?
- How does one write clear every filter’s key at once?
- What happens when Valkey answers slower than 100 ms?
- 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/