Problem brief
Twitter (X) lets users post short messages, follow other users, and see a home timeline made up of tweets from everyone they follow, roughly in reverse-chronological order. The read path — building that timeline fast for hundreds of millions of users — is the whole problem. It's a classic interview question precisely because the obvious answer ("query tweets from everyone I follow, sort, return") breaks down completely once follower counts and timeline-read volume are pushed to real scale.
New to feed systems? Here's the whole idea in 30 seconds.
You follow 200 people on Twitter. When you open the app, you expect to see a home timeline: a merged, roughly-time-ordered feed of tweets from all 200 of them, loading in well under a second.
The naive approach: when you open the app, look up your 200 follows, fetch their recent tweets, merge and sort by time, return the top 20. That's a fan-out on read — all the work happens at read time.
The problem: some accounts have 50 million followers. If just one of them tweets, and fan-out work happened at read time, that's fine — but if instead you precompute every follower's timeline the moment a tweet is posted (fan-out on write), one tweet from that account triggers 50 million writes. Both extremes have a breaking point, which is exactly why real feed systems use both, chosen per-account.
The two operations, in plain terms:
- Post a tweet (write): "Store this tweet, and get it in front of my followers' timelines."
- Read a timeline (read): "Give me a merged, ordered feed of tweets from everyone I follow, fast."
Everything else in this problem — fan-out strategy, caching, sharding, the celebrity edge case — exists to make that read fast without making that write impossibly expensive.
Practice workspace
Guided practice or a locally saved 45-minute mock.
Step 1 of 8
Functional Requirements
What are the essential flows, and what would you explicitly leave out of version one?
Reference solution hidden
Try answering in the workspace above first. Reveal the full worked solution — data model, architecture, and trade-offs — whenever you want to compare.
Open the full solution page →Reference solution walkthrough
The sections below are a worked solution, starting with requirements and continuing through the data model, architecture, and trade-offs. Use the practice workspace above first if you want to answer without seeing the reference.
Functional Requirements
The core features we need to support:
- Post a tweet — up to 280 characters, optionally with media.
- Follow / unfollow a user.
- Home timeline — a merged, reverse-chronological feed of tweets from everyone the current user follows.
- User timeline (profile) — all tweets from a single user, in order.
Example. Alice follows Bob, Carol, and a celebrity account with 50M followers. Alice opens the app and expects a single feed showing Bob's tweet from 2 minutes ago, the celebrity's tweet from 10 minutes ago, and Carol's tweet from an hour ago — merged and ordered by time, in one request that returns in well under a second.
Out of scope for a first pass: replies/threads, retweets, likes, search, direct messages, and ranking (we assume reverse-chronological, not an ML-ranked "For You" feed). Call these out so the interviewer knows you're scoping deliberately — this problem is hard enough on its own.
Scale Requirements
Always anchor the design with rough numbers. A reasonable set of assumptions:
- 300M daily active users, each posting on average a few times a week → roughly 6,000 tweets/sec at peak (bursty around major events).
- Each user checks their timeline multiple times a day → roughly 300,000 timeline reads/sec at peak — a read:write ratio of 50:1 or more.
- Average follower count is a few hundred, but the distribution has a long tail: some accounts have tens of millions of followers.
- 5 years of tweet retention at ~300 bytes/tweet, ~200B tweets total → roughly 60 TB of raw tweet text (media stored separately, in object storage).
The big takeaways: this is overwhelmingly read-heavy, so the design should push as much work as possible to write time and serve reads from a cache — but the follower-count long tail means a single strategy applied uniformly (pure fan-out-on-write) breaks on the exact accounts that matter most.
Non-Functional Requirements
We pull these from what makes a feed feel responsive and trustworthy:
- Low latency — a timeline load should feel instant (low hundreds of milliseconds), even under peak load.
- High availability — the app must stay usable even during partial outages; a slightly stale timeline beats a broken one.
- Eventual consistency is fine — it's acceptable if a tweet takes a few seconds to appear in a follower's timeline; it is not acceptable for the read path to be slow or down.
- High write throughput with bursts — major live events (elections, sports) cause enormous, unpredictable tweet-rate spikes that the write path must absorb without falling over.
Availability and low read latency are favored over strict freshness — this system is built around "the feed is fast and mostly fresh," not "the feed is always perfectly up to the second."
Data Model
A few purpose-built stores, not one general schema:
| Store | Shape | Notes |
|---|---|---|
| Tweet store | tweet_id (PK), author_id, text, media_url, created_at | sharded by tweet_id or author_id; the source of truth for tweet content |
| Follow graph | (follower_id, followee_id) edges | needs fast "who does X follow" and "who follows X" lookups — a graph or adjacency-list store |
| Timeline cache | timeline:{user_id} -> list of tweet_ids | precomputed per-user feed, kept in Redis; this is what fan-out-on-write populates |
| User profile store | user_id, follower_count, ... | used to decide fan-out strategy (see below) |
Tweet content is stored once and referenced by id everywhere else — the timeline cache stores ids, not full tweet bodies, so a single popular tweet isn't duplicated across millions of cached timelines.
API Endpoints
Three endpoints cover the core flows:
POST /api/tweets
{ "text": "hello world" }
-> 201 { "tweet_id": "...", "created_at": "..." }
GET /api/timeline?cursor={opaque_cursor}&limit=20
-> 200 { "tweets": [...], "next_cursor": "..." }
POST /api/follow
{ "followee_id": "..." }
-> 200 { "ok": true }
Use a cursor-based pagination (an opaque token encoding the last-seen tweet's timestamp/id) rather than page numbers — a live, constantly-appending feed makes offset-based pagination skip or duplicate items as new tweets arrive between page loads.
High-Level Design
The request flow:
- Write path — a user posts a tweet; the tweet service stores it, then hands off to a fan-out service (via a message queue, so the post itself returns immediately without waiting for fan-out to finish). The fan-out service looks up the author's followers and pushes the new tweet id onto each follower's cached timeline.
- Read path — a user requests their timeline; the app reads the precomputed timeline directly from cache — a fast key lookup, no merging or querying followees at request time.
Decoupling posting from fan-out via a queue is what keeps tweet-posting fast and available even when fan-out for a huge account is still working through millions of followers in the background.
Deep Dive: Fan-out on Write vs. Fan-out on Read
This is the heart of the problem — deciding when the work of "get this tweet in front of followers" happens.
- Fan-out on write (push model, good default for most accounts) — the moment a tweet is posted, push its id into every follower's precomputed timeline cache. Timeline reads become a single cheap cache lookup — exactly what we want given the 50:1+ read:write ratio. The cost: the write work scales with follower count. For an account with 500 followers, that's 500 cheap cache writes — trivial. For an account with 50 million followers, that's 50 million cache writes from one tweet — the "celebrity problem," and it can overwhelm the fan-out workers and flood follower timelines that may never even be viewed.
- Fan-out on read (pull model) — do no work at write time; at read time, look up who the user follows, fetch their recent tweets, merge and sort. The cost: a user who follows 200 people now triggers 200 lookups (or a fan-in query) on every single timeline load — and this is the read path, the one happening 50-100× more often than writes. Doing that work on every read is exactly backwards for a read-heavy system.
- Hybrid (what real systems do) — use fan-out on write for the vast majority of accounts (bounded, cheap, and it keeps reads fast), and switch to fan-out on read specifically for accounts above a follower-count threshold (say, 1 million+). When a normal user's timeline is assembled, it merges their precomputed cached feed (from everyone they follow who fans out on write) with a small number of live lookups for the handful of celebrity accounts they follow (fetched fresh, on read, since those accounts skip the write-time fan-out).
Worked example. Alice follows Bob (500 followers, uses fan-out-on-write) and a celebrity (50M followers, flagged for fan-out-on-read). When Bob tweets, it's pushed into Alice's cached timeline immediately. When the celebrity tweets, nothing is pushed anywhere — instead, when Alice opens her timeline, the timeline service reads her precomputed feed (which already has Bob's tweet) and separately fetches the celebrity's most recent tweets directly, merging both by timestamp before returning.
This split is the standard answer real systems (Twitter, Instagram) converged on, and naming the celebrity problem unprompted is usually what separates a strong answer from a mediocre one on this question.
Deep Dive: Sharding and Caching
A few more decisions once the fan-out strategy is set:
- Shard the tweet store by
author_id— most access patterns (fetching a user's tweets, fanning out on write) key off the author, so co-locating a user's tweets avoids cross-shard fan-out for the common case. - Shard the follow graph separately — "who follows X" and "who does X follow" are both hot lookups with a different access shape than tweet storage, and benefit from a graph-oriented store or a dedicated adjacency-list schema.
- Cache timelines in Redis, not the tweet store — the timeline cache holds only tweet ids (small), with a TTL and an eviction policy for inactive users; full tweet content is hydrated from the tweet store (itself cache-fronted) in a batch lookup when the timeline is served, so a popular tweet's text lives in exactly one place.
- CDN for media — images and video never touch the core write/read path described above; they're uploaded directly to object storage and served through a CDN, referenced from the tweet by URL.
Wrap-Up & Trade-offs
A solid answer hits these points:
- Hybrid fan-out — fan-out on write for most accounts to keep reads cheap; fan-out on read for the celebrity long tail to keep writes from exploding.
- Decoupled write path — posting a tweet returns immediately; fan-out happens asynchronously via a queue, so one huge account's fan-out job never blocks anyone's ability to post.
- Timeline cache stores ids, not content — keeps the cache small and avoids duplicating a popular tweet across millions of follower feeds.
- Sharding tweets by author, the follow graph separately — each store is sharded around its own dominant access pattern rather than forced into one shared shard key.
- Favor availability and low read latency over strict freshness — an eventually-consistent, cache-served feed is the right trade for this problem.
Common follow-ups: how to rank a feed instead of showing it strictly reverse-chronological, how retweets and replies change the fan-out model, how to handle a follower-count threshold that an account crosses live (switching strategies without a visible gap), and how a live event (a sudden 100× spike in tweet rate) stresses the queue and fan-out workers differently than steady-state load.