Skip to content

Design Twitter

Timelines at scale, the celebrity hot-key problem in full, trending topics and real-time delivery.

Advanced18 minUpdated 27 September 2026

Twitter looks like Instagram with text instead of photos. It is not, and the differences are exactly what makes it a separate interview question.

Three things change the design:

  • Posts are tiny. No media pipeline, no object storage problem. All the difficulty moves into the timeline.
  • The follower graph is far more extreme. Instagram celebrities have millions of followers; Twitter accounts have hundreds of millions, and they post frequently.
  • It is real-time. Users expect a post within seconds, not on next refresh.

If you have read the Instagram case study, treat this as the deep dive into the one problem that case study introduced.

Phase 1: Clarify

In scope: post a tweet, follow users, view a home timeline, view a user's own timeline.

Out of scope: direct messages, search, ads, media, spaces, moderation.

Non-functional:

  • Extremely read-heavy.
  • Timeline load under 200ms.
  • New posts visible within a few seconds — eventual consistency is acceptable, and saying so early prevents you designing something needlessly expensive.
  • Availability over consistency: a failed timeline load is worse than a slightly stale one.

Phase 2: Estimate

Assume 300M daily actives, 500M tweets a day, and each user loading their timeline 10 times daily.

Writes: 500M ÷ 86,400 ≈ 5,800 tweets per second. Peak perhaps 3x, so ~17,000/s.

Reads: 300M × 10 = 3B timeline loads/day ≈ 35,000 per second, each returning ~20 tweets, so 700,000 tweet-reads per second.

Storage: a tweet with metadata is roughly 300 bytes. 500M × 300B = 150 GB/day — genuinely small. Text is cheap.

The conclusion to state out loud:

"Storage is trivial; the entire problem is read volume and fan-out. At 700,000 tweet reads per second, timelines must be precomputed — no query can run per request at that rate."

Phase 3: API and data model

POST /v1/tweets                       { text }        -> { tweetId }
GET  /v1/timeline/home?cursor=...                     -> { tweets[], nextCursor }
GET  /v1/users/{id}/tweets?cursor=...
POST /v1/users/{id}/follow
users     (id, handle, name, created_at)
tweets    (id, user_id, text, created_at)
follows   (follower_id, followee_id)   PK (follower_id, followee_id)

tweets.id should be a time-sortable distributed id — Snowflake-style — so ids are unique across shards and sort chronologically without a lookup. That single choice removes the need for a coordinating sequence and makes cursor pagination natural.

Phase 4: The timeline

This is the whole question. Two pure strategies, both wrong on their own.

Fan-out on read queries all your followees at request time. Writes are trivial; reads are impossible at 35,000 timeline loads per second when a user follows a thousand accounts.

Fan-out on write precomputes each user's timeline as a list in Redis when someone posts. Reads become a single list fetch — perfect.

Until you compute the write cost.

The celebrity problem, quantified

An account with 100 million followers posts once.

Fan-out on write means 100 million list insertions for one tweet. If that account posts ten times in an hour, that is a billion writes. And several such accounts posting near a major event produces a fan-out storm that no cluster absorbs gracefully.

Worse, most of those writes are wasted. The overwhelming majority of those followers will not open the app before the tweet is buried.

The hybrid, and where the threshold goes

  • Ordinary accounts (below, say, 10,000 followers) → fan out on write. Cheap, and it makes reads instant for almost everyone.
  • High-follower accounts → do not fan out. Their tweets are fetched at read time.
  • Timeline read = precomputed list, merged with recent tweets from the small number of large accounts you follow, sorted by id.
Conceptual diagramnoyesNew tweetFollowers > threshold?Insert into eachfollower's timeline listSkip fan-outTimeline requestRead precomputed listMerge recent tweets fromfollowed large accountsSort by tweet id
Diagram, as text
  • New tweet leads to Followers > threshold?
  • Followers > threshold? leads to Insert into each follower's timeline list (no)
  • Followers > threshold? leads to Skip fan-out (yes)
  • Timeline request leads to Read precomputed list
  • Read precomputed list leads to Merge recent tweets from followed large accounts
  • Merge recent tweets from followed large accounts leads to Sort by tweet id

The merge is cheap because the number of large accounts any one user follows is small — typically under a hundred — and their recent tweets are heavily cached.

Real-time delivery

Instagram can wait for a refresh. Twitter feels live, which needs a push channel.

WebSockets keep a persistent connection per active user, so new tweets are pushed as they arrive. The cost is state: connections are long-lived and pinned to a specific server, so you need a connection registry mapping user to server, and the load balancer must handle sticky, long-lived connections.

At 300M daily actives with millions concurrent, that is a substantial fleet of connection servers, separate from your API servers so they scale independently.

Server-sent events are a simpler one-way alternative when clients only receive. Long polling is the fallback for restrictive networks.

The design point: the connection layer is its own service. Timeline generation publishes to a queue; connection servers subscribe and push to the users they hold.

A different shape of problem — high-volume aggregation rather than fan-out.

Counting every hashtag exactly, in real time, across 17,000 tweets per second, is expensive and unnecessary. Trends do not need to be exact.

The practical approach is stream processing over approximate counts: tweets flow into a log, a stream processor maintains counts in sliding windows, and probabilistic structures like Count-Min Sketch give approximate frequencies in bounded memory.

"Trending" is also not raw volume — it is rate of change. A hashtag used constantly is not trending; one that jumped 50x in ten minutes is. Say this, because it shows you understood the product requirement rather than just the counting mechanism.

Bottlenecks and failure

Redis memory for timelines. Cap each timeline at a few hundred tweets and only maintain them for recently-active users. Dormant users fall back to pull.

Fan-out workers falling behind. During a spike, the fan-out queue grows and timelines lag. Acceptable degradation — but monitor queue depth, since it is the leading indicator from the message queue lesson.

Hot shard on a celebrity's own tweets. Their tweets are read enormously. Cache aggressively; the data is immutable once posted.

Connection server failure. Clients reconnect and are assigned elsewhere. Timelines must be fetchable over plain HTTP so a WebSocket outage degrades to refresh-to-update rather than breaking the product.

Check your understanding

Common mistakes

Choosing fan-out on write or on read as a doctrine. The interesting answer is neither: it is the hybrid, and the reason for it. A typical user's followers can have the post pushed to them cheaply; an account with ten million followers cannot, so those are pulled at read time. Picking one strategy for everyone is the mistake.

Forgetting that the celebrity case is the whole problem. If follower counts were uniform, one strategy would work and there would be no question to ask. The skew is the design constraint, and a solution that does not mention it has not engaged with it.

Treating the timeline as a database query. Assembling a home timeline by joining posts to follows at read time works at a thousand users and collapses well before a million. The timeline is a materialised, maintained artefact, not a query result.

Ignoring deletes and privacy changes. A post pushed into ten million timelines does not un-push itself when it is deleted or the account goes private. Every fan-out design owes an answer for how the copies are retracted or filtered at read.

Unbounded timeline storage. Keeping every post ever pushed to every follower grows without limit. Timelines are capped, and old entries are re-fetched rather than stored forever.

No plan for the write spike. A single event can make millions of people post within the same minute. The ingestion path has to absorb that without the read path noticing.

Practice

Design the threshold that decides whether an account uses fan-out on write or fan-out on read. What would you measure, and what happens to accounts that cross it?

Write your own answer first, on paper or in a note. Then compare.

Show a hint

The threshold is where the two costs meet, and it moves as the product grows.

Show a strong answer

The threshold sits where per-follower write cost equals read-merge cost.

  • Fan-out on write cost for one post ≈ (number of followers) × (cost of one timeline write).
  • Fan-out on read cost ≈ (number of that account's followers who open their timeline) × (cost of merging one extra source).

Setting them equal shows the crossover depends on how active the followers are, not just how many there are. An account with 100,000 mostly-inactive followers is cheaper to fan out on read than one with 50,000 daily-active followers.

What I would measure:

  • The distribution of follower counts, to see where the tail begins — usually a very small number of accounts sit far above everyone else.
  • Cost per timeline write and per read-time merge, measured rather than assumed.
  • Follower activity rate, because that is what makes the naive follower-count threshold wrong.

A practical starting point is tens of thousands of followers, tuned from the measurements, and expressed as a configurable value rather than a constant in code.

What happens when an account crosses it is the part that is easy to get wrong:

  • Crossing upward: stop fanning out on write. Existing timeline entries stay — do not delete them — and new posts are merged at read time. Followers see no discontinuity.
  • Crossing downward (follower count drops): resume fan-out on write, but do not backfill history. Old posts remain read-merged.
  • Add hysteresis. An account oscillating around the threshold would flip strategy repeatedly. Use different thresholds for switching up and down so it settles.

The subtle requirement: the read path must always merge both sources, regardless of the account's current mode, because history was written under whichever mode applied at the time.

A user complains their timeline shows a reply before the post it replies to. Explain how this happens and what you would do about it.

Write your own answer first, on paper or in a note. Then compare.

Show a hint

Ask what the timeline is ordered by, and whether that ordering respects causality.

Show a strong answer

How it happens: timeline entries are ordered by a timestamp or ID assigned at creation, and those come from different machines. If the reply was created on a machine whose clock is slightly ahead, or written to a different shard that was fanned out faster, its position can precede the post it answers. Wall-clock ordering does not respect causality — a reply caused by a post can carry an earlier timestamp.

Why it matters more than it looks: an out-of-order feed is cosmetically odd, but the same mechanism produces genuinely confusing results — a "deleted" marker arriving before the item, or an edit applied before the original.

What I would do, in increasing order of cost:

  • Order by a monotonic ID rather than wall-clock time. A Snowflake-style ID is time-ordered and generated without cross-machine coordination, which removes clock skew as a cause while keeping generation cheap.
  • Carry the parent reference and repair at read time. If a reply is rendered and its parent is present but later in the list, the client places it after the parent. This fixes the visible symptom cheaply, and is what most feeds actually do.
  • Track causal ordering explicitly — each post records the ID it replies to, and the merge step enforces that a reply never precedes its parent. This is correct and costs a dependency check per item on the read path.

What I would not do: try to synchronise clocks tightly enough to make timestamps causal. Clock synchronisation reduces skew but never eliminates it, and building correctness on "clocks are close enough" fails intermittently and unreproducibly — the worst failure profile there is.

Key takeaways

  • Twitter's difficulty is not storage — 150GB a day is trivial. It is read volume and fan-out.
  • At ~700,000 tweet-reads per second, timelines must be precomputed; no per-request query survives that rate.
  • Fan-out cost equals follower count. Fine at 500, catastrophic at 100 million — so apply it below a threshold and pull above it.
  • Use time-sortable distributed ids so tweets order chronologically across shards without coordination.
  • Real-time delivery is a separate stateful service — WebSocket connection servers subscribing to a queue, scaled independently, with HTTP fallback.
  • Trending is rate of change, not volume, and approximate counting in bounded memory is the right tool.
  • Cap timeline length and maintain them only for active users, falling back to pull for everyone else.

Further reading

  • Nathan Bronson et al., "TAO: Facebook's Distributed Data Store for the Social Graph" (USENIX ATC 2013).
  • Graham Cormode and S. Muthukrishnan, "An Improved Data Stream Summary: The Count-Min Sketch and its Applications" (Journal of Algorithms, 2005).
  • Leslie Lamport, "Time, Clocks, and the Ordering of Events in a Distributed System" (CACM, 1978) — why causal order is not clock order.
  • Twitter Engineering, "Announcing Snowflake" (2010) — time-sortable IDs without coordination.
  • Jay Kreps, "The Log: What every software engineer should know about real-time data's unifying abstraction" (2013).

Check yourself

3 questions in this lesson. Answer each once to complete it.

Practice this

Next lessonDesign Uber
Lessons in this track