Design a news feed
45 minutes. "Design the home timeline for a social product."
Step 1: clarify (4 minutes)
The questions whose answers change the architecture:
- Scale? 300 million monthly actives, 100 million daily. Assume it.
- Follow graph shape? Median follower count and the maximum. This is the question, because the whole design turns on the tail.
- Ordering: chronological or ranked? Ranked changes the read path completely.
- Freshness requirement? "New post visible in the follower's feed within N seconds." Say 10 seconds for most, immediately for the author's own view.
- Read to write ratio? Assume 100:1, which is typical and is the fact that drives everything.
Then state the non-functional requirement that governs the design: the read path must be a lookup, not a computation. At 100:1 read to write, any work you can move to write time is work done 100 times less often.
Step 2: capacity math (3 minutes)
Writes
100M DAU x 0.5 posts/day = 50M posts/day = ~580 posts/sec average
Peak 3x = ~1,700 posts/sec
Reads
100M DAU x 10 feed views/day = 1B reads/day = ~11,600 reads/sec average
Peak 3x = ~35,000 reads/sec <- the design driver
Fan-out volume (the number that decides the architecture)
Median follower count ~200
580 posts/sec x 200 = 116,000 timeline writes/sec average
Peak: ~350,000/sec
Storage
Post: ~500 B of metadata (text in a blob store) x 50M/day = 25 GB/day
Timeline cache: 100M users x 800 entries x 24 B (post_id + score + author)
= 100M x ~19 KB = ~1.9 TB, sharded across a Redis fleet
Two conclusions, derived rather than asserted. 35,000 reads per second cannot come from a relational primary, so the read path is a cache. And 350,000 timeline writes per second is large but tractable, which is what makes fan-out on write viable at all.
Step 3: the three architectures
Fan-out on write (push). When you post, write your post id into every follower's precomputed timeline.
Read: O(1) lookup of a cached list. ~2 ms
Write: O(followers) writes. expensive for large accounts
Fan-out on read (pull). Store posts once. On read, look up who you follow, query their recent posts, merge.
Read: O(following) queries + merge. ~200 ms for 500 follows
Write: O(1). cheap
Hybrid. Push for normal accounts, pull for high-follower accounts, merged at read time. This is what everyone actually runs, and the reason is the follower distribution.
| Push | Pull | Hybrid | |
|---|---|---|---|
| Read latency | Excellent | Poor | Good |
| Write cost | O(followers) | O(1) | O(followers) for most |
| Celebrity post | Catastrophic | Free | Handled by the pull path |
| Storage | High (duplication) | Low | Moderate |
| Inactive users | Wasted work | None | Fixed by not fanning out to them |
Why the hybrid is forced
The follower distribution is a power law. A worked version of the tail:
Median account: 200 followers -> 200 writes per post. Fine.
99th percentile: 20,000 followers -> 20,000 writes. Acceptable.
Top accounts: 50,000,000 followers -> 50M writes for ONE post.
At 1,700 posts/sec, a single celebrity post is 50M timeline writes.
Sustained, that is more write volume than the entire rest of the system.
So: push below a follower threshold, pull above it. The threshold is an operational constant, tuned so that the pull path handles a small number of accounts and the push path is not dominated by them. Somewhere in the tens of thousands is typical.
Step 4: the write path
CELEBRITY_THRESHOLD = 50_000
def publish(author_id, content):
"""One post. The interesting decisions are all about who NOT to write to."""
post_id = snowflake_id() # time-sortable, no coordination
posts.put(post_id, author_id, content, ts=now()) # source of truth
follower_count = graph.follower_count(author_id)
if follower_count >= CELEBRITY_THRESHOLD:
return post_id # pull path: readers merge this at read time
# Fan out asynchronously. The author's own timeline is written
# synchronously so they see their post immediately (read-your-writes).
timeline.push(author_id, post_id)
fanout_queue.publish({"post_id": post_id, "author_id": author_id})
return post_id
def fanout_worker(msg):
"""Partitioned by author so one hot author cannot starve the fleet.
Batched because 200 individual Redis writes is 200 round trips."""
for batch in chunked(graph.followers(msg["author_id"]), 1000):
active = activity.filter_active(batch, within=timedelta(days=30))
timeline.push_many(active, msg["post_id"]) # single pipelined write
Three decisions worth defending:
Only fan out to active users. In a mature product a large fraction of accounts are dormant. Writing to their timelines is pure waste, and skipping them can cut fan-out volume substantially. Dormant users get the pull path on the rare occasion they return, which is also when their timeline gets rebuilt.
The author's own timeline is written synchronously. Otherwise the author posts, refreshes, and does not see their post, which is the single most reported bug in any eventually-consistent feed. This is the same read-your-writes fix as in CQRS.
Fan-out is partitioned by author id, so one high-volume author saturates one partition rather than the whole worker fleet.
Step 5: the read path
FEED_PAGE = 50
def get_feed(user_id, cursor=None):
"""Two sources merged. The pull leg is small because the celebrity set
per user is small, and it is cacheable across all followers."""
pushed = timeline.range(user_id, cursor, limit=FEED_PAGE * 2) # ~1 ms
celebs = graph.followed_celebrities(user_id) # usually < 50
pulled = []
for author in celebs:
pulled += author_recent_cache.get(author, limit=FEED_PAGE) # shared cache
merged = merge_by_score(pushed + pulled)[:FEED_PAGE]
hydrated = posts.multi_get([m.post_id for m in merged]) # batch fetch
return hydrated, next_cursor(merged)
The pull leg looks expensive and is not, for a reason worth stating: a celebrity's recent posts are the same for every follower, so that cache entry is read by millions of users and written once. It is the highest-hit-rate object in the system.
Timeline truncation. Cap each timeline at roughly 800 entries. Almost nobody scrolls past a few hundred, and deep pagination falls back to a slower path. This is what makes the 1.9 TB storage estimate hold rather than growing without bound.
Ranking, if the feed is ranked rather than chronological: the cached timeline holds candidates, and a light ranker scores the top few hundred at read time within a tight budget (say 30 ms), with the heavy features precomputed. That is the standard multi-stage funnel, and the design point is that candidate generation happens at write time while scoring happens at read time.
Step 6: failure modes and degradation
| Failure | Behaviour | Reasoning |
|---|---|---|
| Timeline cache node lost | Rebuild that shard from posts on demand; serve pull-path only meanwhile | Timelines are derived data, always rebuildable |
| Fan-out workers lag | Feeds go stale; author still sees own posts | Queue depth is the SLI; alert on it |
| Post store unavailable | Serve cached hydrated posts; no new posts | Source of truth, so writes must fail rather than be lost |
| Celebrity cache miss | Fall back to a direct query on that author | Bounded, small number of accounts |
| Ranking service down | Serve chronological | Pre-agreed degradation, not an incident decision |
The principle to state: the timeline is a projection, not a source of truth. It can be rebuilt from posts and the follow graph at any time, which means a corruption incident is a rebuild job rather than data loss. That property is what makes the whole design safe to operate, and it is the same argument as CQRS projections.
The follow-graph change problem, which interviewers like: when A follows B, A's timeline does not contain B's history. Backfill the last N posts asynchronously. When A unfollows B, B's posts remain in A's cached timeline until it rotates; filter at read time or accept a short window of staleness. Unfollow is the one worth naming, because "why do I still see their posts" is a real support ticket.
Step 7: what changes at ten times the scale
At 1 billion daily actives, fan-out volume approaches 3.5 million timeline writes per second at peak. The changes, in order:
- Lower the celebrity threshold, moving more accounts to pull. The threshold is a load-balancing knob between the two paths, not a constant.
- Regionalise. Timelines are read locally, so shard the cache by user region and fan out cross-region asynchronously. Accept that a follower in another region sees a post a second or two later.
- Tiered timelines. Full precomputation for daily actives, on-demand for weekly, nothing for dormant.
- Do not move to pull entirely. It inverts the cost onto the read path, which is 100 times more frequent.
Production evidence
Twitter's timeline architecture has been described in multiple public engineering talks: a fan-out-on-write design writing into an in-memory (Redis) store with timelines capped at a few hundred entries, and a separate path for high-follower accounts merged at read time. The reported motivation is exactly the one above, that read volume dwarfs write volume so precomputation wins, with the celebrity tail as the exception that forces the hybrid.
Meta's TAO (Bronson et al., USENIX ATC 2013) is the read-optimised graph store in front of MySQL that serves the social graph: writes go to the durable normalised store, reads are served by a write-through cache with a graph API. It is the same read-write split at a different layer, and a good thing to name.
Instagram's engineering blog has published on feed ranking as a multi-stage funnel with candidate generation separated from ranking, which is the structure described in step 5.
Redis sorted sets are the standard implementation for the timeline itself
(ZADD with a time-based or ranking score, ZREVRANGEBYSCORE for pagination,
ZREMRANGEBYRANK for truncation), and the fact that the whole timeline layer is one
data structure is part of why the design is operationally simple.
The debate
The case for pure pull: no duplication, no fan-out infrastructure, no consistency window, and follow-graph changes are instantly reflected. For a product with a small following graph or low read volume it is simply correct, and it is the right place to start.
The case for pure push: simplest read path, and the read path is what users experience. It is viable right up until the first account with a million followers, which is a product success you should expect.
My position: hybrid, with the threshold as a tuned operational constant rather than an architectural commitment. Push for the median account because reads outnumber writes by two orders of magnitude and precomputation is the cheapest possible read. Pull for the tail because the tail is unbounded and would otherwise dominate write volume. And treat the timeline as a rebuildable projection, so a cache incident is a rebuild rather than a loss.
The push path is the wrong choice when the follow graph is dense (everyone follows everyone, as in a small workspace product), when reads are rare relative to writes, or when the feed must reflect follow-graph changes instantly. The pull path is wrong whenever read latency is the product.
Follow-up Q&A
"Push, pull or hybrid, and why?" Hybrid, and the reason is the follower distribution rather than a general preference. Reads outnumber writes about 100 to 1, so precomputing timelines at write time is work done far less often, which argues for push. But the follower count is a power law, so one account with 50 million followers generates 50 million timeline writes per post, which would dominate the entire system. So push below a threshold and pull above it, merging at read time. The pull leg is cheap because a celebrity's recent posts are identical for every follower, so that cache entry has an enormous hit rate.
"How do you handle the celebrity problem?" Do not fan out above a follower threshold. Those authors' recent posts live in a shared cache that every follower reads at feed-assembly time, so the cost is one cached list per celebrity rather than one write per follower. The threshold is an operational knob: lower it to shift load from the write path to the read path. The reason this works is that the number of celebrities any single user follows is small, typically under a few dozen, so the merge is bounded.
"The author posts and doesn't see it in their own feed. Why, and what's the fix?" Fan-out is asynchronous, so the author's timeline has not been written yet. The fix is to write the author's own timeline synchronously in the publish path, and optionally have the client insert optimistically. It is the same read-your-writes problem as any CQRS projection, and the same fix: give the originating user the synchronous path and everyone else the asynchronous one.
"What happens when someone follows a new account?" Their timeline has none of that account's history, so backfill the last N posts asynchronously and merge them into the timeline by score. Unfollow is the harder direction: the unfollowed account's posts are already in the cached timeline and will remain until it rotates. Either filter at read time against the current follow set, which costs a lookup, or accept a short staleness window. Worth naming, because "why do I still see their posts" is a real support ticket.
"The timeline cache loses a shard. What happens?" Nothing permanent, because the timeline is a projection rather than a source of truth: it can be rebuilt from the posts table and the follow graph. In the meantime those users get the pull path, which is slower but correct. This is the property that makes the design safe to operate, and it is why I would keep posts in a durable store and never treat the cache as authoritative. I would also track rebuild time and treat it as an RTO, because that is what it is.
Common misconceptions
The most common is that this is a choice between push and pull. Every system at scale is hybrid; the interesting question is where the threshold sits and what happens at it.
The second is that fan-out on write is expensive because of storage. Storage is cheap; the cost is write amplification, and the fix is not fanning out to dormant users, which is where a large fraction of the waste lives in a mature product.
The third is that the timeline needs to be consistent. It is a feed. A few seconds of staleness for other people's posts is invisible; the only consistency requirement that matters is that authors see their own posts immediately.
Interview delivery note
Open with the arithmetic, because it makes the architecture a consequence rather than a preference: "Reads outnumber writes about 100 to 1, so I want the read path to be a lookup rather than a computation, which argues for fanning out on write. But the follower distribution is a power law, and one account with 50 million followers turns a single post into 50 million timeline writes. So: push below a threshold, pull above it, merge at read time."
Then the two details that show you have thought about operating it: "I'd only fan out to users active in the last 30 days, because in a mature product most accounts are dormant and writing to them is pure waste. And I'd write the author's own timeline synchronously, because otherwise they post, refresh, and don't see it, which is the most reported bug in any feed."
The depth signal is the projection framing: "the timeline is derived data, not a source of truth, so a cache loss is a rebuild job rather than data loss, and I'd measure rebuild time and treat it as an RTO."
Further reading
- Bronson et al., "TAO: Facebook's Distributed Data Store for the Social Graph" (USENIX ATC 2013).
- Public Twitter engineering talks on timeline architecture, for the hybrid design and the Redis-backed timeline store.
- Instagram engineering writing on feed ranking as candidate generation followed by ranking stages.
- Kleppmann, Designing Data-Intensive Applications, chapter 1, which uses the Twitter timeline as its worked example of the write-versus-read cost tradeoff.