Design a News Feed: Fanout, Ranking, and the Celebrity Problem
How to design a scalable news feed — fanout-on-write vs fanout-on-read, the hybrid model, feed caching, ranking, and the capacity math interviewers ask for.
This article has an interactive companion
Don't just read it — step through it interactively, one state change at a time.
Open Instagram, Twitter, or LinkedIn and you see the same thing: a scrolling list of posts from people you follow, freshest and most relevant on top. Building that list for one user is trivial — query their followees, grab recent posts, sort by time. Building it for 300 million users who each expect their feed to load in under 200ms is one of the classic hard problems in system design.
The whole design hinges on a single question: do you build each feed when someone posts, or when someone reads? Get that trade-off right and everything else — caching, ranking, storage — falls into place. Get it wrong and one celebrity tweet takes down your write pipeline.
What the feed actually has to do
Strip the product down to its promises. The functional requirements are small:
- A user can publish a post (text, plus media references).
- A user can read their feed — a merged, ranked list of recent posts from everyone they follow.
- A user can follow and unfollow other users.
- The feed paginates as the user scrolls.
The non-functional requirements are where the design lives:
| Requirement | Target | Why it drives design |
|---|---|---|
| Feed read latency | < 200ms p99 | Reads must hit a pre-built cache, not recompute |
| Availability | Reads > writes | A stale feed beats no feed; reads can degrade gracefully |
| Freshness | Seconds to a minute | New posts appear "soon", not instantly — buys async fanout |
| Consistency | Eventual | Feeds don't need read-your-write; ordering can lag |
| Read:write ratio | ~100:1 | People scroll far more than they post — optimize reads |
That last row is the most important number in the whole design. Feeds are overwhelmingly read-heavy, which is the entire justification for pre-computing feeds at write time rather than assembling them on every read.
Capacity math interviewers actually ask for
Assume 300M daily active users, each following 200 people on average, posting 2 times a day, and refreshing their feed 10 times a day. Do the arithmetic out loud — it exposes exactly where the system will break.
Writes (posts): 300M × 2 = 600M posts/day ≈ 7,000 posts/sec average. That's tiny. But if you push each post into every follower's inbox, the physical write count is 600M × 200 followers = 120 billion inbox writes/day ≈ 1.4M writes/sec. That is a 200x amplification, and it is the reason naive push does not scale.
Reads (feed refreshes): 300M × 10 = 3B/day ≈ 35,000 RPS average, call it 150,000 RPS at peak. Every one of those must be cheap.
Storage: A post at ~1KB (body plus media references) is 600M × 1KB ≈ 600 GB/day, roughly 220 TB/year before replication. Inbox entries hold only a post id (~20 bytes), so they're small per row, but at 120B rows/day they dominate row counts and index pressure.
The takeaway is not the exact figures — it's the shape. Writes to inboxes, not reads, are the bottleneck, and that's counterintuitive for a "read-heavy" system.
Fanout-on-write vs fanout-on-read
There are two ways to answer "what's in this user's feed?"
Fanout-on-write (push): when a user posts, immediately insert that post id into the inbox of every follower. Reading a feed is then a single cheap lookup of a pre-built list. Reads are O(1); writes are O(followers).
Fanout-on-read (pull): store each post once in the author's outbox. When a user reads, gather the outboxes of everyone they follow, merge, and sort. Writes are O(1); reads are O(followees × posts).
| Dimension | Fanout-on-write (push) | Fanout-on-read (pull) |
|---|---|---|
| Read cost | O(1) — read a ready list | O(followees) — gather + merge |
| Write cost | O(followers) — one write each | O(1) — write once |
| Best for | Users with modest follower counts | Users with huge follower counts |
| Failure mode | Write amplification on big accounts | Slow, expensive reads |
| Feed freshness | Instant once fanout completes | Fresh but computed every time |
Push wins for the common case: most users have a few hundred followers, so the write cost is bounded and reads stay dirt cheap. Pull wins only when the follower count is so large that pushing would flood the write pipeline.
The celebrity problem and hybrid fanout
Here's the concrete failure. Suppose a user with 100 million followers posts a single 280-byte tweet. Pure push inserts 100M rows across 100M inbox partitions. At ~230 bytes per inbox row (id, metadata, index overhead) that's roughly 23 GB of write amplification for one tweet. Do that for every celebrity post and the write pipeline saturates, fanout lag stretches into hours, and normal users' posts get stuck behind the backlog.
The fix that every production feed uses is hybrid fanout. Pick a follower threshold — say 10,000. Anyone above it is marked pull-only: their posts are written once to their own outbox and never fanned out. Everyone else stays push. At read time, a user's feed builder does two things:
- Read the pushed inbox — posts from normal followees, already sitting in a ranked list.
- Read the outboxes of the handful of celebrities this user follows, and merge them in.
Reads get slightly more expensive (a few extra outbox fetches) but writes are now bounded. A celebrity post costs one write instead of 100 million. This is the single most important design decision in the whole system.
Walkthrough: building Priya's feed
Trace one concrete read. Priya follows 200 people. 198 are normal accounts (pushed), and 2 are celebrities above the 10K threshold (pull-only): a musician with 40M followers and a cricketer with 90M followers.
| Step | Action | Source | Result |
|---|---|---|---|
| 1 | Feed request arrives for user_id=priya | API gateway | Route to feed builder |
| 2 | Read cached ranked feed | Redis sorted-set feed:priya | Hit — 800 pushed post ids returned |
| 3 | Fetch celebrity outboxes | outbox:musician, outbox:cricketer | 12 recent post ids added to candidate pool |
| 4 | Merge pushed + pulled candidates | In-memory | ~812 candidate post ids |
| 5 | Filter tombstones (blocked/deleted) | tombstones:priya set | Drop 3 blocked-author posts → 809 |
| 6 | Rank candidates | Feature store + ranker | Top 50 selected by score |
| 7 | Hydrate top 50 | posts table join | Post bodies, author info, media URLs |
| 8 | Return page 1 with cursor | API response | 20 posts + opaque cursor for "load more" |
Notice what step 2 did: because normal followees were pushed, Priya's bulk feed was already sitting in Redis. Only the two celebrities needed a live pull. That's hybrid fanout paying off — 198 accounts cost nothing at read time, 2 accounts cost two outbox fetches.
Caching and ranking the feed
Why cache hit rate is everything. Priya's ranked feed — the top ~500 to 1,000 post ids — lives in a Redis list or sorted-set keyed by her user id, refreshed when new posts are pushed and on a periodic re-rank. Building that feed cold means fetching her followees, fetching their recent posts, running the ranker: tens of database and ML calls. At 150,000 peak RPS the database physically cannot serve a cold read for every scroll. Feed systems target 95%+ cache hit rates, with TTLs tuned to the ratio of active to total users. A 95% hit rate turns 150,000 RPS into 7,500 storage reads/sec; a 0% hit rate melts the database.
Ranking beyond reverse-chronological. Modern feeds don't sort by time alone. After candidate generation produces a pool of ~1,000 posts, a feature store fetches signals — author affinity, recency decay, engagement velocity, content type — and a model scores each candidate. It's typically a two-stage pipeline: a fast linear or gradient-boosted pre-ranker trims the pool, then a heavier deep model scores the shortlist. Per-(user, post) scores are cached for a few minutes so pagination stays stable within a scrolling session and a user doesn't see posts reshuffle mid-scroll.
Edits, deletes, and stable pagination
Handling deletions, edits, and blocks after fanout. This is why inboxes store only post ids, never post bodies. At read time a hydration step joins the id list against the canonical posts table, and that join is where you lazily filter deleted posts, posts edited to private, or authors the reader has blocked. A per-user tombstone set caches recent blocks so each candidate is filtered in O(1), and a background job purges tombstoned ids from hot inboxes to keep them clean. Denormalizing post text into inbox rows would make every edit a fanout storm — the indirection is deliberate.
Stable pagination. Use a cursor, not offset/limit. An opaque token encoding the last-seen rank position or timestamp + post_id lets "load more" return items strictly after that point. Offset pagination breaks the moment new posts arrive at the top — everything shifts, and the user sees duplicates or skips. New posts that land while the user is scrolling go into a separate "N new posts" pill at the top rather than being spliced into the middle of the current page. Reading stays stable even under heavy write load.
How this shows up in interviews
The interviewer will almost always steer you toward the celebrity problem. State push vs pull early, then let them ask "what about someone with 100 million followers?" — that's your cue to introduce hybrid fanout with the threshold and the outbox-merge-at-read step. Have the write-amplification number ready (100M followers ≈ 23 GB per tweet); a concrete figure lands harder than "it's a lot of writes."
Expect follow-ups on cache hit rate and what the cold path costs, on ranking (where the ML ranker plugs in and why it's two-stage), on how deletes and blocks work after fanout, and on cursor pagination. Walk one read request end to end, name the source of truth (the posts table), and be explicit about the trade-off you'd reverse if the workload changed — for example, if the average follower count exploded, you'd lower the push threshold.
Further learning
- Designing INSTAGRAM: System Design of News Feed — Gaurav Sen — a clear video walkthrough of feed generation and the push/pull decision.
- Design Decisions for Scaling Your High Traffic Feeds — High Scalability — a production-grounded look at the trade-offs behind large feed systems.
- Practice this topic on YeetCode to work through the design interactively.
Related designs worth studying next: Design a Web Crawler, Design an Autocomplete System, Design a Unique ID Generator, and Design a Task Scheduler.
FAQ
Should I use fanout-on-write or fanout-on-read for a news feed?
Use both. Fanout-on-write (push) pre-builds each follower's feed at post time, making reads a single O(1) lookup — ideal for the vast majority of users who have modest follower counts. Fanout-on-read (pull) stores a post once and assembles feeds at read time, which avoids write amplification but makes every read expensive. Production systems run a hybrid: push for normal accounts, pull for high-follower "celebrity" accounts, then merge the pushed inbox and the pulled celebrity outboxes at read time. The hybrid is not a compromise you settle for — it's the correct answer that the interviewer is fishing for.
What is the celebrity problem in feed design?
It's the write-amplification blowup that happens when a user with a huge follower count posts under a pure push model. A single tweet from an account with 100 million followers would insert 100 million inbox rows — roughly 23 GB of writes for one 280-byte message. Do that for every celebrity post and the write pipeline saturates, fanout falls hours behind, and ordinary users' posts get stuck in the backlog. The fix is to mark high-follower accounts (above a threshold like 10,000 followers) as pull-only so their posts are written once and merged into feeds at read time instead.
Why is cache hit rate so critical for a news feed?
Because rebuilding a feed from scratch is expensive — fetching followees, fetching their recent posts, and running the ranker takes tens of database and ML calls. At scale a feed system handles well over 100,000 reads per second, and the database cannot serve a cold read for every scroll. Storing each active user's ranked feed (the top 500 to 1,000 post ids) in Redis and targeting a 95%+ hit rate is what keeps the read path fast. A 95% hit rate cuts storage reads by 20x; a low hit rate collapses the whole system.
How do you rank feed items beyond newest-first?
After candidate generation gathers a pool of roughly 1,000 posts, a feature store fetches signals such as author affinity, recency decay, engagement velocity, and content type, and a model scores each candidate to pick the top K. This is usually a two-stage pipeline: a fast pre-ranker (a linear model or gradient-boosted trees) trims the pool cheaply, then a heavier deep model scores the shortlist. Per-(user, post) scores are cached for a few minutes so that pagination stays stable and posts don't reshuffle while the user scrolls.
How do you keep pagination stable as new posts arrive?
Use cursor-based pagination instead of offset/limit. The cursor is an opaque token encoding the last-seen rank position or a timestamp + post_id, and each "load more" call returns items strictly after that point. Offset pagination breaks when new content arrives at the top of the feed — everything shifts down, so the reader sees duplicates or skips posts. New items that appear mid-scroll are surfaced through a separate "N new posts" pill at the top rather than being inserted into the current page, which keeps the reading position stable even under heavy write traffic.
Make it stick: run this one yourself
Don't just read it — step through it interactively, one state change at a time.