“Design the home feed for a Twitter-like app” is one of the most common system design prompts, and the core of it is a single trade-off. When someone posts, do you copy the post into every follower’s feed right away (fan-out on write), or do you assemble each feed when the user opens the app (fan-out on read)? Most good answers end at a hybrid, and the interviewer’s real question is whether you can explain where the line goes and why.
Two ways to build a feed
Fan-out on write (push). Each user has a precomputed feed, typically a list of post ids in Redis or a wide-column store. When Ana posts, a background worker looks up her followers and prepends the post id to each of their feeds. Reading a feed is one list read plus a batch lookup of the posts.
Fan-out on read (pull). Each author has a timeline of their own posts. When Ben opens the app, the feed service fetches recent posts from every account he follows, merges them by time (or by a ranking score), and returns the top 50. Posting is one insert.
Push makes reads cheap and writes expensive. Pull does the opposite. Which one costs less depends on two numbers: how often people read versus post, and how followers are distributed. The second one is where naive designs break.
Simulating a real follower distribution
Follower counts on social networks are heavily skewed: most accounts have dozens of followers, and a handful have millions. To see what that does to cost, a Python model generates 20 million users with follower counts from a Pareto distribution (median 30), assumes each user posts 0.5 times a day and opens the feed 8 times a day:
followers = [min(int(17 * random.paretovariate(1.2)), 40_000_000) for _ in range(USERS)]
push_inserts = POSTS_PER_USER_DAY * edges # every post is copied to every follower
pull_reads = USERS * FEED_LOADS_PER_USER_DAY * following_avg # every load reads every followed timelineusers 20,000,000 follow edges 1,906,678,101 mean followers 95 median 30 top account 13,743,211
top 5 accounts: 13,743,211, 10,372,408, 9,979,265, 9,399,493, 8,112,731
fan-out on write: 953 M feed inserts/day = 11,034/s average
one post by the top account = 13,743,211 inserts; at 500,000 inserts/s that takes 27 s
fan-out on read: 1,852 feed loads/s, each merging ~95 author timelines = 176,544 timeline reads/sThe average follower count (95) is three times the median (30), a sign of the long tail. Pure push needs about 11,000 feed inserts per second on average, which a Redis cluster handles comfortably. Pure pull needs about 177,000 timeline reads per second, sixteen times more operations, and each feed load waits on 95 reads before it can merge and rank. With feeds read far more often than posts are written, push wins on average.
Then look at the single line about the top account. One post from the 13.7-million-follower account means 13.7 million inserts. Even at 500,000 inserts per second of dedicated fan-out capacity, the last follower sees the post 27 seconds after the first. Several big accounts posting during the same live event stack up, and ordinary users’ posts queue behind them. That’s the “celebrity problem”, and averages hide it completely.
The hybrid and where the threshold goes
The usual fix: push posts from ordinary authors, and don’t fan out posts from accounts above a follower threshold. At read time, the feed service takes the user’s precomputed feed and merges in recent posts from the few big accounts they follow. Here’s what different thresholds do:
hybrid: push for authors below the threshold, merge the rest at read time
threshold authors pulled inserts/s timelines merged per load timeline reads/s
1,000,000 36 10,376 5.7 10,533
100,000 592 9,645 12.0 22,228
10,000 9,461 8,459 22.2 41,200The surprise for many people is the inserts column. Pulling the 36 accounts with over a million followers cuts average insert volume by only about 6%. That’s because, in this model, those 36 accounts post as often as anyone else, and there are 20 million other accounts. What the hybrid removes is the spike: no single post can trigger millions of inserts anymore, so fan-out latency stays low and predictable for everyone. In real networks big accounts often post more than average, which makes the savings larger, but the main win is still the tail.
The read cost of the hybrid is small at a high threshold. Each feed load merges about 6 celebrity timelines, and those timelines are the most read data in the system, so they sit in cache. Lowering the threshold to 10,000 means merging 22 timelines per load and doubling read operations, for a modest further drop in inserts. A threshold somewhere around 100,000 to 1,000,000 followers is a reasonable starting point here, and the right number comes from measuring fan-out lag and feed latency, not from a formula.
Details that come up in follow-up questions
Inactive users. Pushing to every follower means writing into feeds of people who haven’t opened the app in months. Many designs push only to followers active in the last 30 days and rebuild a feed on demand (pull style) when a dormant user returns.
Feed size and memory. Feeds don’t need to be infinite. Capping each feed at 800 post ids of 8 bytes is 6.4 KB per user, about 128 GB of raw ids for 20 million users before Redis overhead. Scrolling past the cap falls back to pull.
Deletes and privacy changes. If a post is deleted or an account goes private, its id is already in millions of feeds. Rather than chasing every copy, filter at read time: the post lookup drops deleted or no-longer-visible posts. The feed stores ids, never copies of post content, precisely so that edits and deletes take effect everywhere.
Ordering and ranking. A chronological feed merges by timestamp, and post ids that sort by time (Snowflake-style) make that cheap. A ranked feed scores candidates at read time anyway, so it naturally pulls from both the precomputed list and the celebrity timelines before ranking.
Unfollows and new follows. Following someone should backfill a few of their recent posts into your feed; unfollowing should filter their posts at read time until they age out.
A common mistake is presenting push as “write the post to N feeds in the request”. Fan-out belongs on a queue (Kafka partitioned by author, for example) with workers, so the author’s request returns as soon as the post itself is stored and fan-out lag becomes a metric you watch.
Interview practice: a live sports final
“Your feed uses fan-out on write. During a football final, the top 200 sports accounts post every few minutes, each with millions of followers. Users complain that posts from their friends show up ten minutes late. What’s happening and what do you change?”
Answer
The fan-out workers are a shared queue, and the celebrity posts are flooding it. If 200 accounts with an average of, say, 3 million followers each post every five minutes, that’s 600 million inserts per five minutes, about 2 million per second, far beyond the fan-out capacity. A friend’s post with 150 followers waits behind all of it, which is why ordinary posts arrive ten minutes late even though they’re cheap.
The structural fix is the hybrid: stop fanning out posts from accounts above a follower threshold (perhaps 1 million) and merge their recent posts into feeds at read time. Those timelines are hot and cache well, so the read cost is a handful of extra cached lookups per feed load. Short term, I’d also separate the queues: put large-fan-out work on its own lane with its own workers, so a celebrity storm can’t delay ordinary posts. I’d add metrics for fan-out lag (time from post to last feed insert) split by author size, and alert on the ordinary-author lane, since that’s what users notice.