Design the home feed for a social network.
Size it first: 200M daily actives opening the feed six times a day is 42k reads/s at peak, while 100M posts fanned out to 200 followers each is 231k feed writes/s, so writes dominate. Push to ordinary accounts, pull for high-follower ones, and store ids so deletes and blocks filter on read.
What the interviewer is scoring
- Whether the read-to-write ratio is computed rather than assumed to favour precomputation
- Does the candidate derive a follower threshold for the hybrid rather than asserting that celebrities are special
- That the feed store holds references and the read path hydrates, rather than materialised rendered posts
- Whether deletion, blocking and privacy are handled at read time instead of by chasing copies
- Does the candidate give pagination a stable anchor on a feed that changes while the user scrolls
Answer
What I need pinned down before designing
Four answers change the architecture. Whether the feed is chronological or ranked, because a ranked feed cannot store a final ordering at write time — the score depends on when you read and on signals that arrive later. The follower distribution, because the highest-follower accounts decide whether precomputation is viable at all. The retention depth, since a feed that must scroll back a year is a different storage problem from one that keeps the last few hundred items. And whether posts are public, because privacy checks that must be evaluated per viewer cannot be baked into a shared precomputed list.
I will assume 800 million registered accounts of which 200 million are daily active, each opening the feed six times a day; an average of 200 accounts followed and, symmetrically, 200 followers; active users posting an average of half a post each per day; a ranked feed; and a working depth of the most recent few hundred items with older content reachable by a slower path.
The numbers, and which one is the design driver
Reads are 200M × 6 = 1.2 billion feed loads per day, which is 1.2e9 / 86,400 ≈ 13,900 per second on average and, at a 3× diurnal peak, roughly 42,000 per second.
Writes are 200M × 0.5 = 100 million posts per day, about 1,160 per second average and 3,500 at peak. That gives a read-to-write ratio of only 12:1, which is the first useful observation: this is nothing like a URL shortener's hundred-to-one, so the reflex that "reads dominate, therefore precompute everything" is not supported by the numbers yet.
The number that actually decides the design is the fan-out. Delivering each post into every follower's feed is 100M × 200 = 2 × 10^10 row insertions per day, which is 2e10 / 86,400 ≈ 231,000 per second average and around 700,000 per second at peak. That peak figure is roughly seventeen times the peak read rate of 42,000. The system is a write-amplification machine, and every subsequent decision is about controlling that multiplier.
One cheap reduction falls straight out of the assumptions. Only a quarter of registered accounts are daily active, so fanning out to all followers writes three rows for every one that will be read before it ages out. Delivering only to recently-active followers, and rebuilding a dormant user's feed on demand when they return, cuts the sustained fan-out to about 58,000 per second. That single decision is worth more than any storage engine choice.
Feed storage is modest by comparison. A feed entry needs a post id, an author id and a score or timestamp, so about 40 bytes with overhead. Capping each feed at 800 entries gives 32 KB per user, and 200 million active users is 200e6 × 32e3 = 6.4 TB — comfortably sharded, and small enough to keep in a memory-resident tier rather than on disk.
Fan-out on write, fan-out on read, and why neither alone works
Fan-out on write does the work when a post is created: look up the author's followers and append an entry to each of their feeds. Reads then become a single range read of one precomputed list, which is what gives a feed its sub-100 ms load time. The cost is the 231,000 writes per second above, plus a tail latency problem — a post by a well-followed account cannot be delivered synchronously, so it enters a queue and delivery becomes eventually consistent.
Fan-out on read stores nothing per follower. At read time you fetch the recent posts of each followed account and merge them. Writes become one insertion per post, which is trivial, and there is nothing to backfill when someone follows an account. But every feed load becomes a 200-way scatter-gather whose latency is the slowest of 200 shard reads, and at 42,000 reads per second that is 8.4 million backend reads per second. It also breaks the caching story, because there is no single object to cache.
So the answer is neither. Fan out on write for the overwhelming majority of accounts, and pull at read time for the few whose fan-out is pathological.
The threshold should be derived rather than asserted. Fan-out cost is linear in follower count, so an account with 30 million followers generates 30 million insertions from one post — at the 231,000 per second baseline that is about 130 seconds during which that single post consumes the entire global fan-out budget, and every ordinary post queues behind it. Set the cut-off where one post's fan-out stops fitting comfortably inside one delivery batch: with a pipeline sized for a few hundred thousand insertions per second, accounts above roughly 100,000 followers are pulled rather than pushed. That is a starting point tied to your delivery capacity and revisited with measurement, not a universal constant.
Pulling those accounts is cheap precisely because they are few. A user follows at most a handful of them, so the read path merges one precomputed list with a small number of author timelines, each a hot, heavily cached, single-key range read. The extra read-path work is bounded by the number of large accounts a user follows, not by their total following.
Store references, and rank at read time
The feed store should hold post ids, not post content, and certainly not rendered HTML. The same post exists in up to 100,000 feeds, so storing 2 KB per copy instead of 40 bytes multiplies your storage by fifty; an edited post would need every copy rewritten; and — the reason candidates miss — an ordering baked in at write time means every ranking change becomes a fleet-wide backfill instead of a deploy.
So the feed store holds a candidate set, the ids of things that could appear in rough recency order, and the read path scores them. Candidate generation is cheap and mechanical; ranking is a model over per-viewer signals that only exist at read time, such as how recently this viewer engaged with this author. Hydration then fetches the bodies for the top N in one batched multi-get against a post store with a very high cache hit rate, because feeds overlap heavily.
Ranking has a cost worth stating: once the ordering is not chronological, "I have seen everything up to here" is no longer expressible as a timestamp, so the read path must track what this viewer was already shown.
Why deletion decides the storage design
This is the part that separates an answer that has thought the design through from one that has drawn the boxes. A post that is deleted, an account that is blocked, a post whose visibility is narrowed from public to followers-only — none of these can be handled by going back and editing the feeds that contain the reference. There may be 100,000 of them, they are spread across every shard, and the user expects the post to be gone now, not after a background job has walked them all.
So visibility is evaluated at read time, on the candidate set, immediately before hydration. Hydration is doing that work anyway: it fetches each post, and a post that has been deleted simply is not returned, so it drops out of the feed with no cleanup at all. Blocks and mutes are a small per-viewer set that can be held in memory and applied as a filter over the candidate ids. Privacy is a check against the post's current audience, not the audience it had when the reference was written.
That is why the feed store must be treated as a cache of possibilities rather than as a record of truth. It is allowed to contain references to things that no longer exist or are no longer visible, and the read path is required to cope. Design it that way and deletion is free; design it as materialised content and deletion is an unbounded fan-out problem with a user-visible correctness bug at the end of it. It also means you must over-fetch: if you need 20 items and filtering may remove some, request more candidates than you intend to show and top up rather than returning a short page.
Pagination on something that is changing
Offset pagination on a feed is broken for the reason it is always broken, but here it is worse than usual because new items arrive at the front continuously — by the time a user requests page two, the boundary has moved and they will see items they already scrolled past. The fix is to anchor the session. When a feed load begins, fix the position, and have every subsequent page request carry an opaque cursor encoding that anchor plus the position within it. New posts that arrive during the scroll are surfaced as a "new posts" affordance at the top rather than silently injected into the middle of the sequence, which is both correct and the better product behaviour.
What breaks in production
Fan-out lag is asymmetric in a way users notice. Delivery to followers can take seconds, but a user's own post missing from their own view reads as data loss, so the read path merges the viewer's recent posts in locally rather than waiting for the fan-out to come back around.
The delivery queue absorbs the gap between average and peak fan-out, so it needs durability, per-partition ordering and enough retention to survive a consumer outage. Prioritise it by size: a post to 300 followers should not sit behind a 30-million-row delivery, which argues for separate queues by fan-out class rather than one queue in arrival order.
A post read by tens of millions of viewers is a single hot key in the post store, not a feed problem, so it wants hot-key replication across cache nodes or a small in-process cache in front — consistent hashing alone routes every one of those reads to the same node. And a new follow needs a plan, since the followed account's existing posts are not in the feed: backfill the most recent few dozen through the same queue, never synchronously on a bulk follow, with the pull path covering the gap meanwhile.
Likely follow-ups
- A user follows 400 new accounts at once. What has to happen to their feed, and when?
- How do you keep a chronological guarantee once ranking is involved, or do you give it up?
- A single account with 30 million followers posts every ten minutes. Trace what happens.
- How would you test that a ranking change improved the feed rather than just changed it?
Related questions
- How do you decide whether to use a managed service or self-host a component, and which cloud costs catch teams out?hardAlso on capacity-planning6 min
- Design a URL shortener.hardAlso on capacity-planning6 min
- Design the contract for a public API. How do you handle pagination, idempotency and versioning?hardAlso on pagination6 min
- Site search converts poorly. How would you improve relevance when conversion is the metric you are judged on?hardAlso on ranking6 min
- Clients are asking for page 4,000 of your /orders collection. How is that endpoint paginated, and what would you change?mediumAlso on pagination4 min
- Where do you put the cache, and how big does it need to be?hardAlso on capacity-planning6 min
- Adding a second LEFT JOIN doubled the revenue figure on a report. Explain what happened and write the correct query.mediumAlso on fan-out4 min
- Every stakeholder says their item is urgent. How do you decide what goes into next quarter?hardAlso on capacity-planning6 min