Section 7: One URL, Ten Thousand Readers a Second - Deep Dive: The Question Page — Caching and the Read Path
Executive Summary: The candidate dives into the question page first, because 85% of all traffic is answered by the CDN and everything else in the design assumes that number holds. The read path has three layers: the CDN edge and origin shield serve one HTML page to everyone, a Redis bundle cache holds each question's content without its scores, and read replicas answer the small, live parts (scores and the signed-in reader's own state). Content writes purge the page, coalesced to at most one purge per question every 5 seconds. Votes never purge. Single-flight rebuilds turn a purge on a 280/s viral page into one database rebuild, and the comment tree is cut to 200 comments before it is ever rendered.
Section 6's boxes are on the board. The interviewer caps the marker: "You've got about twenty minutes. Where do you want to go deeper?"
You: "Three dives, in this order. First the question page, because it is the product: 300M page views a day land on it, and the whole capacity plan rests on the edge absorbing 85% of them. If I get caching wrong, every other number in Section 3 is wrong. Second, votes in Section 8, because they are 86% of writes and carry the one invariant that can't bend: one vote per person per post. Third, search and duplicate detection in Section 9, because a duplicate question splits the answers and lowers quality for every future reader. Page first."
Dive A: Three Layers, Each Shrinking the Load
Here is the funnel at peak, using the numbers from Section 3.
| Layer | What it holds | How it stays fresh | Hit ratio | What gets through |
|---|---|---|---|---|
| CDN edge + shield | Full HTML, identical for every reader | s-maxage=60 + SWR 60; purged on content writes |
85% | ≈1,600 renders/s |
| Bundle cache (Redis, 150 GB working set) | Question, answers, comment skeleton, rendered bodies. No scores | Deleted on content writes; LRU | 95% | 80 rebuilds/s |
| Read replicas | Scores, viewer-state, rebuild queries | Async replication | n/a | ≈4,000 queries/s total |
The contract at the edge is two responses with opposite cache rules:
GET /questions/{id}/{slug}
200 OK
Content-Type: text/html; charset=utf-8
Cache-Control: public, max-age=0, s-maxage=60, stale-while-revalidate=60, stale-if-error=86400
Surrogate-Key: q-{id} # purge tag (the header name varies by CDN)
Vary: Accept-Encoding # never Vary: Cookie, never Set-Cookie
GET /api/v1/questions/{id}/viewer-state
200 OK
Cache-Control: private, no-store
{ "myVotes": { "<daniel-answer-id>": 1 }, "canEdit": ["<priya-question-id>"], "bookmarked": true }
Interviewer: "Signed-in and anonymous readers get the same HTML? Then how does Priya see their own upvote lit up?"
You: "The page paints the content, then one small call fetches viewer-state with a p95 of 100 ms, and the script lights up Priya's arrow and shows the edit link. The HTML never knows who's reading it, so it stays one cached copy for everyone."
Why do scores sit outside the bundle? Votes arrive at up to 350/s. Put scores in the bundle and you have two bad choices: invalidate it on every vote, so the cache never stays warm, or leave the scores stale until the next content write, which on a quiet question could mean weeks. Instead, every origin render does one score lookup on the question's shard, about 1,600/s at peak. If those lookups ever become the bottleneck, the fix is a small per-question score cache with a 10 s TTL. It would not mean putting scores back in the bundle.
Quick check before moving on:
Sign in to continue reading
The rest of this lesson is available with a free account. Signing in with Google or Microsoft is free.
Sign in to read the full lesson