If you remember five things
The whole answer in one glance. Everything below is the defense of these five calls.
- Hybrid fan-out. Push tweets to followers at write time for normal users, pull for the few celebrities, and merge the two at read time. This one call is driven entirely by the read:write asymmetry.
- Durable write, async fan-out. Save the tweet and acknowledge before fan-out runs, so posting feels instant and a fan-out backlog can never block or lose a write.
- Two separate caches. Per-user timeline lists and shared tweet content get hot in completely different ways, so you defend them separately.
- Bounded, lazy timelines. Cap each list near 800 IDs, backfill on follow, tombstone on delete, and only keep active users warm to control the memory bill.
- Read-your-writes patch. Eventual consistency everywhere, except your own just-posted tweet, which you always see instantly. That is the one gap users actually notice.
1Clarifying the requirements#
Pin down what you are on the hook for before drawing a single box. Scope decisions here change the architecture, so treat them as load-bearing, not small talk. Here is how I would open:
Candidate: When you say "Twitter," do you want the full product, or should I focus on the core post-and-read loop? I would rather go deep on the hard part than sprinkle attention everywhere.
Interviewer: Focus on posting tweets, following people, and the home timeline. Search, ads, and DMs are out of scope.
Candidate: Got it. Is the home timeline chronological or ranked by relevance?
Interviewer: Assume reverse-chronological for now, but design it so a ranking layer could slot in later.
Candidate: Last one: what is the read-to-write ratio you expect, and do we care about a celebrity with 100 million followers?
Interviewer: Reads dominate heavily, and yes, assume we have celebrities. That is where I want you to spend time.
What those answers just told me:
- The interview is about fan-out and hot keys, not CRUD.
- "Ranking could slot in later" means keep timeline IDs and tweet content separable, so a scorer can reorder IDs before hydration.
- "Reads dominate, celebrities exist" is the interviewer handing me the thesis of the whole design.
Functional requirements
- Post a tweet (up to 280 chars, optional media).
- Follow and unfollow other users.
- Load the home timeline: recent tweets from people you follow, newest first.
- View any single user's profile timeline (their own tweets).
- Likes and retweets (a retweet is just a tweet that references another).
Non-functional requirements
- Read-heavy. Timeline reads should feel instant, p99 under about 200 ms.
- Availability over strict consistency. A friend's tweet 3 seconds late is nobody's bug; the site being down is everybody's.
- Timelines can be eventually consistent. A posted tweet reaching followers within a couple of seconds is fine.
- Scale to hundreds of millions of daily users, plus the celebrity fan-out problem.
- Durable. Once I see "posted," that tweet must not vanish.
This opening is scored before you draw a single box. I am listening for whether you pull out the read:write asymmetry and the celebrity case on your own. Start sketching a login flow or CRUD endpoints and I have quietly downgraded you. Name the hard part first, then go spend your time there.
2Back-of-the-envelope estimation#
Get the scale in your head, because it decides the architecture. One number drives everything, and I will flag it when I reach it.
You are not here to compute storage to three decimal places. You are here to surface one load-bearing number: reads dwarf writes, and one write can fan out to millions of timeline updates. That asymmetry is what justifies precomputing on write and serving nearly free on read. Find it fast, then move on.
Users and traffic.
- 300 million daily active users.
- Each refreshes the timeline about 10 times a day, so 3 billion reads/day.
- Over 86,400 seconds that is about 35,000 reads/second average.
- Peak is spiky, so plan for 3x: roughly 100,000 timeline reads/second.
Writes.
- Posting is far rarer than reading. Say each user posts twice a day.
- 300M x 2 = 600 million tweets/day, about 7,000 writes/second average.
- Call it 20,000 writes/second at peak.
The asymmetry (this is the one that matters).
- Raw read:write ratio is only about 5:1.
- But one write can fan out to millions of timeline updates, while one read touches a single precomputed list.
- The real write-to-work ratio hits tens of millions to one for a top account.
- This is why I precompute on write and serve nearly free on read.
Storage.
- A tweet is small: about 280 bytes for plain ASCII, up to ~1 KB with multibyte characters and emoji, plus metadata.
- Call it 500 bytes to 1 KB per tweet with indexes.
- 600M tweets/day x 1 KB = 600 GB/day of text, about 220 TB/year.
- Media is far bigger, so it goes to object storage and a CDN, never the tweet DB: roughly 27 TB/day to blob storage.
Timeline cache (the number that bites).
- Keep the latest ~800 tweet IDs per active user in memory: 800 x 16 bytes is about 13 KB per user.
- For 300M active users that is about 4 TB of RAM, just for the timeline index.
- That is a cluster of cache nodes, not one box, and it becomes a cost lever later.
Bandwidth.
- 100K reads/s, each returning ~20 hydrated tweets at ~1 KB = 20 KB per response.
- That is about 2 GB/s outbound for hydration, before media. Media rides the CDN.
3Core APIs#
Keep these small and boring on purpose. Clean APIs are a feature.
POST /v1/tweets
GET /v1/timeline/home
GET /v1/timeline/user/{userId}
POST /v1/follows
DELETE /v1/follows/{targetUserId}
POST /v1/tweets/{tweetId}/like
Posting a tweet:
POST /v1/tweets
{
"text": "shipped the thing",
"media_ids": ["m_8842"],
"reply_to": null
}
200 OK
{
"tweet_id": "tw_90af31",
"author_id": "u_1024",
"created_at": "2026-11-03T09:12:44Z"
}
Two design choices worth defending:
- Media uploads first to a separate endpoint that returns
media_ids, so the tweet write stays tiny and big bytes never touch the hot path. - The POST returns the moment the tweet is durably stored, before fan-out runs.
Reading the home timeline uses cursor pagination, not offset:
- Offset pagination on an infinite feed is a trap: new tweets at the head shift everything, giving duplicates or gaps.
- The cursor is an opaque, time-encoded token, so the server resumes a range scan on tweet_id without re-counting from zero.
GET /v1/timeline/home?limit=20&cursor=eyJ0cyI6MTc2fQ
200 OK
{
"items": [
{ "tweet_id": "tw_90af31", "author_id": "u_1024", "text": "...", "created_at": "..." }
],
"next_cursor": "eyJ0cyI6MTc1fQ"
}
4High-level design#
The box diagram is table stakes. What earns points is the single architectural seam: a message queue between the tweet write and the fan-out. That seam lets posting feel instant, absorbs spikes, and guarantees a fan-out backlog can never block or drop a write. Draw the boxes fast, then defend that seam.
The spine is standard, so draw it fast:
- Clients hit a gateway.
- The gateway routes to a handful of services.
- Each service owns its own data.
- All the interesting choices are about how the home timeline gets built and cached.
Write path.
- You POST a tweet.
- The Tweet Service writes it durably to the Tweet Store (the source of truth) and assigns an ID.
- It drops a "new tweet" event onto the Fanout Queue.
- The HTTP response returns right after the durable write, so posting feels instant.
- Fan-out happens asynchronously behind the queue.
Read path.
- You GET your home timeline.
- The Timeline Service reads your precomputed list of tweet IDs from the Timeline Cache (a sorted list per user).
- It hydrates those IDs into full tweets via the cache-backed Tweet Service.
- Hydration is one batch read: 20 IDs is a single multi-get, not 20 lookups.
Why split it this way.
- Reads and writes have opposite shapes: reads are enormous, latency-sensitive, and cacheable; writes are smaller and can tolerate a queue.
- The queue decouples "the tweet is saved" from "the tweet reached 40 million timelines."
- A fan-out backlog can never slow down or drop a write.
Sortable unique IDs.
- I want tweet IDs roughly time-ordered, so I can range-scan and paginate without a separate sort key.
- A snowflake-style 64-bit ID (timestamp bits + machine bits + sequence) does this, shards nicely, and lets me derive
created_atfrom the ID.
The one snowflake hazard a senior interviewer will poke: the clock.
- Timestamp bits assume a monotonic wall clock, so an NTP step backward or a repeated leap second could reissue a timestamp and collide.
- The guard: the generator remembers the last timestamp it emitted and refuses to go backward, waiting out the drift instead.
- Sequence bits (say 12, giving 4,096 IDs per millisecond per node) cover a hot millisecond; exhaustion rolls to the next millisecond rather than wrapping.
- Result: no coordination, bounded skew tolerance, duplicates structurally impossible.
5Data model#
Use a few stores, each picked for its access pattern. Tweets and the social graph are different beasts, so I do not force them into one database.
Tweets (wide-column store like Cassandra, partitioned by author, with a global ID index):
| Field | Type | Notes |
|---|---|---|
| tweet_id | bigint | Snowflake ID, time-sortable, primary key |
| author_id | bigint | Who posted it |
| text | text | Up to 280 chars |
| media_ids | list | Pointers into object store, nullable |
| reply_to | bigint | Nullable, for threads |
| retweet_of | bigint | Nullable, references original tweet |
| created_at | timestamp | Also derivable from tweet_id |
| like_count | counter | Denormalized, eventually consistent |
- Partitioning by author_id makes a profile read ("everything u_1024 posted") a single-partition range scan, the hot path for
GET /v1/timeline/user/{userId}.
Follows (the social graph; I need fast "who do I follow" and "who follows me"):
| Field | Type | Notes |
|---|---|---|
| follower_id | bigint | The user doing the following |
| followee_id | bigint | The user being followed |
| created_at | timestamp | When the follow happened |
- Store the edge twice, indexed both directions: by follower_id for "who do I follow" (pull), by followee_id for "who follows me" (fan-out).
- Doubling the write is cheap; follows are rare compared to reads.
Home timeline (lives in the cache tier, not a disk DB). Per user, a sorted list of tweet IDs capped near 800:
| Field | Type | Notes |
|---|---|---|
| user_id | bigint | Owner of this timeline, the cache key |
| tweet_ids | sorted set | Score = tweet_id so it sorts by time, trimmed to 800 |
- IDs only, not full tweets: keeps the per-user footprint near 13 KB, lets many users share one copy of a popular tweet's content, and is the seam where a ranking layer reorders IDs before hydration.
Users sit in a relational store keyed by user_id with profile fields and follower_count. Nothing exotic.
6Deep dives#
6.1 Fan-out: the central decision#
This is the heart of the problem. When you post, how do your followers see it? Two ways.
- Fan-out on write (push). At post time, push your tweet ID into every follower's home timeline list in the cache. Reads become trivial; writes become expensive.
- Fan-out on read (pull). Store nothing per follower. At read time, fetch who they follow, read recent tweets from each, and merge-sort by time. Writes are trivial; reads become expensive.
Fan-out on write (push)
- Read cost
- Trivial: read one precomputed list.
- Write cost
- Heavy: one post touches every follower's list.
- Best when
- Reads dominate and most accounts are normal-sized.
- Breaks when
- A celebrity posts: one write becomes tens of millions.
Fan-out on read (pull)
- Read cost
- Heavy: fetch and merge from everyone you follow.
- Write cost
- Trivial: store nothing per follower.
- Best when
- Writes dominate, or the author has a huge following.
- Breaks when
- A read-heavy feed makes every timeline load expensive.
For a read-heavy product, push wins almost everywhere: precompute the expensive thing once at write, serve it millions of times for free. So push is my default.
Reveal the answer
Then the celebrity breaks pure push.
- 90 million followers, one post, 90 million cache writes.
- Do that during a spike and the fan-out queue backs up for minutes; everyone's timeline goes stale.
- That one hot author produces more write amplification than the rest of the site combined.
So I go hybrid.
- Most users get push.
- Users above a follower threshold (say 1 million) are flagged "hot" and get pull.
- At read time, a timeline is the precomputed push list merged with a fresh pull of the handful of hot accounts you follow.
- A normal user follows maybe 2 or 3 celebrities, so the pull side is tiny and fast.
And the read side with the merge:
The threshold is a knob, not a law.
- Too low and too many accounts go pull, hurting read latency.
- Too high and a mid-tier account with 800K followers hammers fan-out.
- Tune it from real data, and make it dynamic per posting rate, not just follower count.
- A 1.2M-follower account that posts weekly is cheaper to push than a 600K account that posts 50 times an hour.
Retweets make the merge sneaky.
- A retweet is its own tweet row with
retweet_ofpointing at the original. - Three friends retweeting the same post means three entries racing into your list, plus the original if you follow the author.
- Merge naively and you show the same content four times.
- Fix: dedup on the referenced content ID, not the tweet ID. Collapse entries sharing a
retweet_of, keep the earliest surfaced one, attribute it to whoever surfaced it first.
Fan-out is at-least-once, and that is fine here by construction.
- A worker retry can push the same tweet ID twice.
- No dedup table needed: the timeline is a sorted set scored by tweet_id, so re-adding an existing ID is a no-op.
- Idempotency falls out of the data structure. Say this out loud; the interviewer is watching for whether you noticed retries can duplicate.
Committing to pure push, feeling clever about cheap reads, then freezing when I say "a celebrity just posted." Reach for hybrid on your own, and say why in numbers. Candidates who wait to be told about the celebrity case are the ones who have not actually run this system.
6.2 Hot keys and the thundering herd#
Separate from the celebrity-posting problem is the celebrity-being-read problem.
- When a huge account posts, millions pull that same tweet within seconds.
- In a cache that is one key getting hammered: a hot key.
- The node holding it gets cooked while its neighbors idle.
A few defenses, used together:
- Replicate hot entries. Copy hot tweet IDs to multiple cache nodes (or a small per-host local cache in front of the shared tier) to spread the read load.
- Request coalescing. If 50,000 requests for the same uncached tweet arrive at once, one fetches and the rest wait on that single in-flight lookup. This kills the thundering herd on cache misses.
- Hydration cache. The tweet-content cache (ID to full tweet) is separate from the timeline-list cache. Hot content lives here with a short TTL and serves from memory for the vast majority of reads.
Knowing a key is hot in real time is the actual engineering, and it is the first follow-up I ask:
- At the cache client, sample a fraction of reads and keep an approximate frequency sketch (a count-min sketch, or the LFU counters Redis already maintains) over a sliding window; a key that crosses a rate threshold gets promoted.
- At the proxy, track per-key QPS and flag anything spiking past roughly 10x the median key.
- The promotion action is local-host caching: once flagged, every app host caches the key in process for a few seconds, so the shared node stops seeing the stampede almost immediately.
- Detection need not be perfect or instant, because request coalescing already caps the blast radius of a miss. Detection just decides what to replicate.
The key insight: the two caches fail differently, so defend them separately.
- Timeline lists are per-user and rarely hot individually.
- Tweet content is shared and gets viciously hot.
- One viral tweet is a single key read tens of millions of times; one user's timeline list is read a handful of times by that one user.
6.3 Keeping timelines bounded and consistent#
A list cannot grow forever.
- Cap it near 800 IDs and trim on every push.
- Follow 500 people and your working view is the last few hundred tweets; nobody scrolls past that in a session.
- If they do, fall back to a pull for the tail.
A new follow needs backfill.
- Their last N tweets should appear in my timeline, so I do a one-time backfill: pull their recent tweets and merge them in.
- Unfollow is lazier: do not scrub immediately, filter at read time or let trimming age them out.
- Correctness here is eventual, which our requirements allow.
Deletes matter for trust.
- If you delete a tweet, it must disappear.
- I do not chase 40 million timeline lists to yank one ID.
- I keep a tombstone (deleted tweet IDs) and filter during hydration: a list referencing a deleted ID drops it.
- Cheap, correct, and the stale ID ages out naturally past the 800-entry cap.
6.4 Read-your-writes for your own posts#
This one looks small and costs trust if you skip it.
- Because fan-out is async, you can post, refresh in under a second, and not see your own tweet yet.
- To users that reads as "the post failed," even though it is safely stored.
- Fix: make your own posts strongly consistent while everyone else's stay eventual.
- On a home-timeline read, the Timeline Service also pulls your own most recent tweets straight from the Tweet Store and merges them in, bypassing the fan-out delay.
- The client also shows your tweet optimistically the instant the 200 returns.
- Your own posts are consistent; your friends' posts are eventual. That split matches exactly what users notice.
6.5 Making the write atomic (the dual-write trap)#
There is a correctness bug hiding in "save the tweet, then enqueue the fan-out event."
- Those are two different systems with no shared transaction.
- DB write commits, process dies before the enqueue: the tweet exists but reaches no timeline.
- Enqueue first, DB write fails: workers fan out a tweet that does not exist.
- This is the dual-write problem, and it is the first thing a senior interviewer pokes at. Harden it before they ask.
The clean fix is the transactional outbox.
- In the same transaction that stores the tweet, write a row to an
outboxtable in the same partition. The commit is atomic: both land or neither does. - A relay process (or change-data-capture tailing the commit log) reads new outbox rows, publishes them to the fan-out queue, and marks them done.
- If the relay crashes mid-flight, it republishes on restart, which is just the at-least-once delivery I already tolerate (and the sorted-set push already makes idempotent).
- Cost: a little latency and a background relay. Payoff: "I saw posted" and "my followers will get it" can never diverge. CDC off the commit log preserves per-partition order for free.
Almost every candidate draws "write to DB, then put a message on the queue" and never notices it is two writes with no transaction. Naming the dual-write problem unprompted, then reaching for the outbox or CDC, is a clear staff-level signal.
6.6 Media upload#
Keep big bytes off the hot write path entirely:
- The client asks the media service for an upload slot and gets back a pre-signed URL.
- It uploads the image or video straight to object storage, so bytes never pass through the application tier.
- That returns a
media_idin a pending state. - A transcode pipeline generates variants asynchronously (thumbnails, HLS renditions for video) and flips the media to ready.
- The tweet can reference a
media_idthat is still processing; the CDN serves it the moment it is ready. - This is why the tweet write stays tiny: it carries pointers, never payloads.
7Bottlenecks and trade-offs#
The fan-out queue is the pressure point.
- Under a surge (a global event, everyone posting at once), workers fall behind and timelines go stale.
- The queue absorbs the burst so writes never block, and hybrid already removed celebrities from push.
- Scale workers on queue lag (age of the oldest unprocessed event), not raw depth, because depth lies during a burst of tiny events.
- When lag crosses an SLO (say 5 seconds), shed push in priority order: dormant users first, then low-engagement timelines, and let those rebuild on read.
- Partition the queue by author, so one celebrity's storm cannot head-of-line block everyone else's tweets.
Cache memory is the cost driver.
- 4 TB of RAM for timeline lists is real money. Only keep push timelines for active users.
- Gone a week? Evict the precomputed list and rebuild on demand on return.
- Rebuild cost, not hand-waved: a returning user who follows 500 people needs the newest handful from each, mostly cached hot tweets, so a few hundred batchable reads merged and trimmed to 800, comfortably under a couple hundred milliseconds.
- Serve the first page from a fast partial rebuild and finish the rest async, so nobody waits on a cold timeline.
- The timeline cache is a derived view, never the source of truth: a lost node loses nothing permanent, because any list rebuilds from the Tweet Store plus the follow graph. That is why evicting dormant users is free, not lossy.
Consistency trade-off.
- Availability and eventual consistency everywhere the timeline touches.
- The one case that would feel broken, not seeing your own just-posted tweet, is handled by the read-your-writes patch in 6.4.
- Everything else relaxes: a friend's tweet two seconds late is invisible, and buys the ability to stay up during a spike.
Single points of failure.
- The gateway, the ID generator, and the queue all get clustered, with no single node able to take the system down.
- The ID generator must never hand out duplicates and must survive a node loss, so each node gets its own machine-id bits and generates independently.
- No central counter, no lock, no shared state to lose.
Observability and SLOs.
- SLOs as measured targets: timeline read p99 under 200 ms, and fan-out freshness (post to visible) under a couple of seconds at the median.
- Watch: fan-out queue lag, cache hit ratio per tier (list vs content), hot-key count, p99 read latency, write-path error rate.
- When a number moves, I already know which lever it maps to.
Multi-region (out of scope unless asked, then sketch honestly).
- Reads served region-local from a local cache and replica.
- Writes routed to the author's home region to keep one ordering authority.
- Follow graph and tweet store replicated asynchronously across regions.
- Cross-region timelines are eventually consistent, which our requirements already allow.
Rate limiting and abuse.
- Token bucket at the gateway, per user and per IP.
- Write caps matter more than read caps: one abusive high-follower account is a fan-out weapon.
- Cheap to add, and the interviewer notices when you raise it unprompted.
Where pull would have won.
- For a low-follower, write-heavy product (a private group chat), pull is simpler and cheaper and I would skip fan-out entirely.
- I only pay for push because reads outnumber writes so lopsidedly.
Always let the read:write ratio pick the architecture, not habit.
8Summary#
The decisions that matter:
- Hybrid fan-out. Push for the many, pull for the few celebrities, merged at read time. The single most important call, driven entirely by the read:write asymmetry and the hot-author problem.
- Two separate caches. Per-user timeline lists and shared tweet content fail differently, so I defend them independently, with replication and request coalescing for hot content.
- Durable write, async fan-out. The tweet is saved and acknowledged before fan-out runs, so posting is instant and a backlog never blocks writes or loses data.
- Bounded, lazy timelines. Cap near 800, backfill on follow, tombstone on delete, keep only active users warm.
- Eventual consistency with a read-your-writes patch, so the one case users notice (seeing your own post) stays correct while everything else relaxes.
- Out of scope by choice: search, DMs, ads, notifications, multi-region. Naming them keeps me honest about what 45 minutes covered.
- The ranking seam is concrete: after the merge produces ordered IDs, a scoring service reorders them by a model (recency, affinity, engagement) before hydration. Pure chronological is just the identity ranker.
"The DB write succeeds but the fan-out enqueue fails. Now what?"
"How do you actually know a tweet is a hot key in real time?"
"A worker retries and pushes the same tweet twice. Duplicate in my feed?"
"Three friends retweet the same post. Do I see it three times?"
"Snowflake relies on wall-clock time. What about NTP stepping backward?"
What you can now explain in the room
- Why a read-heavy feed precomputes on write and serves nearly free on read.
- Why pure push breaks on celebrities, and how hybrid fan-out fixes it.
- How to derive the load numbers: 100,000 reads/second at peak and the 4 TB timeline cache.
- Why the tweet write and the fan-out enqueue need an outbox to stay atomic.
- How you detect and defend a hot key, and why the two caches fail differently.
- Why your own posts are strongly consistent while everyone else's are eventual.
For how specific companies probe this one, see our related guides on the engineering interviews at major social and infra companies.
Get a prep plan built around your Design Twitter / X interview
Tell us the role and your interview date. We will send back a dated, day by day plan. Free.