Twitter Newsfeed System Design
The Twitter home timeline (also called newsfeed) is the chronologically- or relevance-ranked stream of tweets a user sees on the app’s home page, drawn from the accounts they follow. It is the single most-discussed system in modern interview prep because it crystallizes one design dichotomy — fanout-on-write (push the tweet to every follower’s inbox at write time) versus fanout-on-read (pull from each followed author at read time) — into a problem with no clean answer, only a hybrid. Twitter’s published architecture (Krikorian 2013 talk, multiple engineering blog posts) does both: push for the typical user with thousands of followers, pull for the rare celebrity user with tens of millions. The interview question is so canonical that interviewers expect the candidate to know this hybrid by name. Failing to discuss the celebrity hot-key problem signals unfamiliarity with the genre. Beyond fanout, the design exercises distributed identifier generation (Twitter’s Snowflake), specialized timeline storage (Manhattan, Cassandra), search via real-time inverted indexes (Earlybird), and the shifting balance between chronological and ranked ordering that Twitter introduced around 2016.
1. Why This System Appears in Interviews
The interview value of “design Twitter timeline” comes from the layered tradeoffs:
- The naive design (every read joins follow-list with all tweets, sorts by time) doesn’t scale past hobby project.
- The first improvement (precompute timelines at write time, push tweets to follower inboxes) buys you most of the way — but breaks on celebrities who would generate billions of inbox writes per tweet.
- The second improvement (pull from celebrity authors at read time) fixes the hot-key but requires merging push-and-pull paths in a single query.
- Each layer introduces new operational concerns: timeline materialization storage, the celebrity threshold (when does a user “become a celebrity”?), backfill on new follow, search separately because the inverted index does not fit the same shard model.
Interviewers test whether the candidate sees the layers and reasons through them, vs. just naming “fanout” without depth.
2. Requirements
2.1 Functional Requirements
- Post a tweet. A user creates a tweet (≤ 280 characters, possibly with media, links, hashtags, mentions). The tweet appears in the followers’ home timelines.
- Home timeline. Reading the home timeline returns recent tweets from followed accounts, in (originally) reverse-chronological order; later, in a relevance-ranked order.
- User timeline. A separate listing of just one user’s own tweets (their profile page).
- Follow / unfollow. Edges in the social graph. Triggers backfill (new tweets from the followed user appear; existing tweets may or may not, depending on policy).
- Like / retweet / reply. Engagement actions; retweets propagate the tweet through the retweeter’s followers’ timelines; likes don’t propagate but generate counts.
- Search. Full-text search over tweets, plus hashtag and user search.
- Notifications. Out of scope here (Notification Service System Design).
- Direct messages. Out of scope (separate subsystem).
2.2 Non-Functional Requirements
- Scale. ~ 200M+ DAU; ~500M tweets/day historical (Twitter circa 2013–2018; current numbers vary).
- Latency. Home timeline p99 < 200 ms; tweet post p99 < 200 ms; tweet visible in followers’ timelines within seconds.
- Availability. 99.99% on read; 99.9% on write.
- Hot users. Some accounts have 100M+ followers; the system must handle their tweets without a per-tweet O(100M) write cascade taking down the service.
- Search latency. Tweet must be searchable within ~10 seconds of posting (Earlybird claim).
These are deliberately round design-budget figures from the 2013–2018 era (Krikorian’s QCon talk and engineering blog posts), not current disclosures. Twitter reported ~150M+ active users at the time of the Timelines at Scale talk (Krikorian, transcribed by High Scalability); the peak write record of 143,199 tweets/s was set during the August 2013 “Castle in the Sky”/Balse broadcast in Japan, shattering the prior 33,388 tweets/s record (Hollywood Reporter, 2013). Post-2022, X no longer discloses these numbers consistently, so the exact current scale is unknown — but the design exercise does not depend on the precise figure, only on the order of magnitude. Use these as a sizing budget, not as authoritative current statistics.
3. Capacity Estimation
3.1 Tweet Write QPS
tweets/day = 5 × 10^8
seconds/day = 86,400
average tweet QPS = 5 × 10^8 / 86,400
≈ 5,800 tweets/s
peak tweet QPS (5×) ≈ 29,000 tweets/s
About 30K tweets/s at peak in our steady-state budget. Twitter’s published 2013 numbers reported a one-second peak of 143,199 tweets/s during the “Castle in the Sky” Japanese TV broadcast (fans tweeting “balse” in unison), versus a prior record of 33,388 tweets/s — well above steady-state, but the system absorbed it (Hollywood Reporter, 2013). This is a peak-per-second number, not a sustained rate; the lesson for the design is that the write path must tolerate burst spikes an order of magnitude above the mean.
3.2 Read QPS
DAU = 2 × 10^8
home timeline reads per DAU/day ≈ 30
home timeline reads/day = 6 × 10^9
average read QPS ≈ 6 × 10^9 / 86,400 ≈ 70,000 reads/s
peak (5×) ≈ 350,000 reads/s
About 350K timeline-read QPS at peak. Each timeline read returns ~20 tweets, so per-tweet fetch QPS is 7M/s on the timeline-fetch path. Heavy caching is the only way this is feasible.
3.3 Fanout Volume (the headline number)
Average follower count, weighted by activity (active users are followed by more active users):
average followers per active user ≈ 200
(heavy-tailed; median is much lower; this is a mean)
fanout writes per tweet ≈ 200
total fanout writes/day = 5 × 10^8 × 200 = 10^11 writes/day
average fanout QPS ≈ 10^11 / 86,400 ≈ 1.16 × 10^6 writes/s
peak fanout QPS ≈ 6 × 10^6 writes/s
A million inbox writes per second on average. This is the headline scaling pressure of fanout-on-write — and why for celebrities (followers ≈ 10⁸) you cannot afford to do it.
3.4 Storage
tweet object size (incl. metadata) ≈ 1 KB
tweets/year = 5 × 10^8 × 365 ≈ 1.8 × 10^11 tweets/year
tweet storage/year ≈ 1.8 × 10^11 × 1000 bytes ≈ 180 TB/year
ten-year tweet storage ≈ 1.8 PB
3× replication ≈ 5.4 PB
Plus, materialized timelines (the per-user inbox of tweet IDs):
average inbox size = 800 tweet IDs (recent only, capped)
tweet ID size = 8 bytes (Snowflake)
per-user inbox size = 6.4 KB
total inbox storage (per-user × DAU) = 6.4 KB × 2 × 10^8 = 1.28 TB
Surprisingly small — the inbox is just a list of IDs, not full tweet objects. The full tweets are fetched separately by ID at read time.
4. API Design
4.1 Post Tweet
POST /api/v2/tweets
Authorization: Bearer <JWT>
{
"text": "Just watched the launch — incredible.",
"in_reply_to": null, // or tweet_id
"quoted_tweet": null,
"media_ids": ["mediaobj_xyz"],
"geo": null
}
→ 201 Created
{
"tweet_id": "1789234567890123456", // 64-bit Snowflake ID
"created_at": "2026-05-08T14:23:11Z",
"author_id": "12345"
}
4.2 Home Timeline
GET /api/v2/timeline/home?max_id=1789...&count=20
Authorization: Bearer <JWT>
→ 200 OK
{
"tweets": [ <tweet object>, ... ],
"next_max_id": "1789234561234567890"
}
4.3 Follow
POST /api/v2/users/<target_id>/follow
→ 204 No Content
The follow operation triggers an asynchronous backfill: the followed user’s recent tweets get added to the follower’s inbox. Backfill is not synchronous to the response; the user sees their friend’s tweets appear within seconds.
5. Data Model
5.1 Tweets — The Source of Truth
Stored in a sharded distributed key-value store (historically Twitter’s Manhattan, before that Cassandra and MySQL).
Table: tweets
Partition key: tweet_id (Snowflake — naturally distributes)
Columns:
author_id BIGINT
text VARCHAR(280)
created_at TIMESTAMP (also embedded in tweet_id)
reply_to_id BIGINT NULL
quoted_id BIGINT NULL
retweet_of_id BIGINT NULL
media_ids LIST<BIGINT>
hashtags LIST<STRING>
mentions LIST<BIGINT>
is_deleted BOOLEAN
Note: tweet_id is Snowflake, which encodes the timestamp directly (41 bits of milliseconds since Twitter’s epoch). This means sorting by tweet_id is equivalent to sorting by creation time, which we exploit heavily in timeline storage.
5.2 Materialized Home Timelines (the Inbox)
For each user, a list of tweet IDs in reverse chronological order (or by ranking score). This is the output of fanout-on-write.
Table: home_timelines (in Redis Cluster)
Key: user:<user_id>:home
Value: sorted set of (tweet_id, score)
capped at 800 most recent entries
Stored in Redis (specifically, ZSET sorted-set type) for fast head/tail access. The 800-entry cap is the actual Twitter number, not a guess: Krikorian stated “your home timeline sits in a Redis cluster and has a maximum of 800 entries,” with each timeline replicated 3× across machines (High Scalability transcription of Timelines at Scale). It is a hard limit: fanout-on-write only delivers to the most-recent 800 slots; older tweets fall off and require a different (slower) “deep timeline” path to retrieve.
5.3 Social Graph (Follows)
Two denormalized tables, both required (asymmetric directional):
Table: followers
PK: user_id
Value: set of user_ids who follow this user
Table: following
PK: user_id
Value: set of user_ids this user follows
For a user with 100M followers (a celebrity), the followers row is enormous — physically a single row containing 100M IDs is unworkable; in practice it’s chunked or sharded out by partition. See §8.2.
5.4 Engagement Counters
Likes, retweets, replies are counters per tweet. Same pattern as Reddit votes (see Distributed Counter System Design) — eventually-consistent sharded counters; events flow through Kafka and aggregate asynchronously.
6. High-Level Architecture
flowchart TB Client -->|HTTPS| Edge Edge --> LB[L7 Load Balancer] LB --> WriteSvc[Tweet Write Service] LB --> ReadSvc[Timeline Read Service] WriteSvc -->|store tweet| TweetStore[(Tweet Store<br/>Manhattan/Cassandra<br/>partitioned by tweet_id)] WriteSvc -->|emit event| Kafka Kafka --> Fanout[Fanout Service] Fanout -->|fetch followers| Graph[(Social Graph<br/>followers / following)] Fanout -->|push to inbox<br/>NORMAL users only| Inbox[(Home Timeline Inbox<br/>Redis ZSET per user)] Kafka --> Search[Earlybird Indexer] Search --> SearchIdx[(Inverted Index<br/>Earlybird)] Kafka --> Counter[Engagement Counter Agg] ReadSvc -->|read inbox| Inbox ReadSvc -->|fetch celeb tweets| TweetStore ReadSvc -->|hydrate tweet bodies| TweetCache[Tweet Cache<br/>Redis] ReadSvc --> TweetStore ReadSvc -->|merge push + pull| Output[Merged Timeline] Output --> Client
What this diagram shows. The architecture is structured around the dual-path read (the central insight). On the write side, every tweet flows into the tweet store and is also emitted as an event into Kafka. The fanout service reads these events; for normal-population tweets it fetches the author’s follower list and writes the tweet ID into each follower’s inbox (a Redis sorted set). For celebrity tweets, fanout is skipped — the tweet sits in the tweet store waiting to be pulled at read time. On the read side, the timeline read service reads the user’s inbox (which contains tweets from everyone they follow except celebrities) and separately pulls recent tweets directly from each celebrity the user follows. The two streams are merged into a single timeline. The search subsystem consumes the same Kafka stream into Earlybird’s specialized real-time inverted index. Counters (likes, retweets, replies) flow through their own aggregator. The tweet cache in front of the tweet store covers the high-overlap “fetch tweet body by ID” workload that timeline rendering produces.
7. Request Flow / Sequence
7.1 Post Tweet (Normal User)
sequenceDiagram participant U as User participant W as Write Service participant ID as Snowflake ID Gen participant TS as Tweet Store participant K as Kafka participant F as Fanout Service participant G as Social Graph participant IB as Inbox (Redis) U->>W: POST /tweets {text, ...} W->>ID: next_id() ID-->>W: tweet_id (Snowflake) W->>TS: PUT tweet_id → {text, author_id, ...} TS-->>W: ok W->>K: emit TweetEvent {tweet_id, author_id, follower_count_hint} W-->>U: 201 Created {tweet_id} Note over K,F: asynchronous from here F->>K: consume TweetEvent alt author is normal (followers < threshold) F->>G: GET follower_ids(author_id) G-->>F: [user_id_1, user_id_2, ..., user_id_N] loop for each follower (parallel batched) F->>IB: ZADD home:<user_id> score=tweet_id member=tweet_id F->>IB: ZREMRANGEBYRANK home:<user_id> 0 -801 (cap to 800) end else author is celebrity Note over F: skip fanout entirely;<br/>tweet pulled at read time end
The user gets 201 Created after the tweet store write — they do not wait for fanout to complete. Fanout is fire-and-forget. The tradeoff: a fast follow-up “did my tweet appear” check from the author themselves may briefly show empty (the tweet is in the store but their own inbox hasn’t been updated yet); typically fanout completes within seconds for normal users.
7.2 Read Home Timeline
sequenceDiagram participant U as User participant R as Read Service participant IB as Inbox participant G as Social Graph participant TS as Tweet Store participant TC as Tweet Cache U->>R: GET /timeline/home R->>IB: ZREVRANGE home:<user_id> 0 99 IB-->>R: [tweet_id_a, tweet_id_b, ...] (push-fanned tweets) R->>G: GET celeb_ids(user_id) G-->>R: [celeb_a, celeb_b, ...] par fetch celebrity tweets in parallel R->>TS: SELECT * FROM tweets WHERE author=celeb_a AND created_at > <cursor> R->>TS: SELECT * FROM tweets WHERE author=celeb_b AND created_at > <cursor> end TS-->>R: pulled celebrity tweets R->>R: merge & sort by tweet_id (= time) par hydrate tweet bodies R->>TC: MGET tweet:a, tweet:b, ... end TC-->>R: tweet bodies (cache miss → fall back to TS) R-->>U: rendered timeline
The merge step combines push-delivered tweet IDs (from inbox) with pulled celebrity tweets, sorts by tweet_id (which is time-ordered thanks to Snowflake), and trims to the page size. The hydration step turns IDs into full tweet bodies.
8. Deep Dive
8.1 Fanout-on-Write vs. Fanout-on-Read vs. Hybrid
These three approaches differ in when the merge work happens.
8.1.1 Fanout-on-Write (Push)
When a tweet is posted, write its ID into every follower’s inbox. Reads are then trivial: read your inbox.
Pros.
- Read latency is constant: one Redis range query.
- Read load is independent of how many people the user follows.
Cons.
- Write amplification = follower count. A user with 1000 followers generates 1000 writes per tweet.
- For celebrities (10⁸ followers), one tweet generates 10⁸ writes — minutes of fanout per tweet, dominating the system’s write capacity.
- Writes for inactive users are wasted — they may not log in for weeks.
8.1.2 Fanout-on-Read (Pull)
When a user reads their timeline, fetch the most recent tweets from every account they follow, merge, sort.
Pros.
- Zero write amplification.
- Inactive users cost nothing.
- New tweets are immediately visible (no fanout lag).
Cons.
- Read latency = time to fetch from N authors. For users following 1000 accounts, this is hundreds of database round-trips.
- Merge cost grows with follow count.
8.1.3 Hybrid (Twitter’s Actual Approach)
Use push for normal users (per-tweet fanout cost is bounded by their follower count). Use pull for celebrities (don’t fan out at all; fetch their tweets at read time). Krikorian described exactly this in the 2013 talk: “For people like Taylor Swift don’t bother with fanout anymore, instead merge in her timeline at read time” (High Scalability transcription). The threshold that defines “celebrity” is operational and Twitter has not published a specific cutoff — the talk frames it qualitatively (the highest-follower accounts) rather than with a number. Below the threshold: push. Above: pull.
The merge happens at read time:
def home_timeline(user_id, count=50):
inbox_tweets = redis.zrevrange(f"home:{user_id}", 0, count-1)
celebrity_followed = graph.celebrities_followed_by(user_id)
pulled = []
for celeb in celebrity_followed:
# last N tweets from each celebrity since user's read cursor
pulled.extend(tweet_store.recent_tweets(celeb, count))
merged = sorted(inbox_tweets + pulled, key=lambda t: t.id, reverse=True)
return merged[:count]The number of celebrities a user follows is small (most people follow a handful of celebrities, not hundreds), so the pull cost stays bounded.
8.1.4 Why “celebrity” Cannot Be Push
A user with 100M followers, posting 5 tweets/day:
fanout writes/day per celebrity = 100M × 5 = 5 × 10^8 writes
seconds/day = 86,400
fanout QPS per celebrity = 5 × 10^8 / 86,400 ≈ 5,800 writes/s sustained
Per one celebrity. With 1000 celebrities active, 5.8M writes/s just for celebrities — this saturates the inbox tier on its own, before any normal users contribute. Pull avoids this entirely; the cost shifts to the read side, where it can be cached aggressively. This matches Krikorian’s stated motivation: at p99, fanout for a high-follower account “could take up to 5 minutes” to reach everyone, even though the p50 to reach the first million recipients was about 3.5 seconds and the system sustained ~30 billion deliveries/day (~300K/s) (High Scalability) — minutes-long tail latency is exactly what the pull path eliminates for whales.
8.2 The Celebrity Hot-Key Problem in Detail
Krikorian’s 2013 QCon talk described the celebrity problem as the defining hard case. He named the specific accounts and their then-current follower counts: @ladygaga (≈31M), @katyperry (≈28M), @justinbieber (≈28M) (High Scalability). A single tweet from one of these accounts means tens of millions of inbox inserts. Push-based fanout for such an account took on the order of minutes per tweet (the p99 fanout latency was “up to 5 minutes”) and would back up the shared fanout queue, delaying delivery for all users.
Solutions explored:
- Pure pull for celebrities. Discussed above. The clean answer.
- Prioritized fanout queue. Celebrity tweets get a lower priority than normal-user tweets so they don’t block the queue. Doesn’t fix the total write volume, only the latency for non-celebrity tweets.
- Fanout to active subset only. Push the celebrity’s tweet only to followers who have been active in the last N days; passive followers get the tweet by pull when they next log in. Significantly reduces write volume since the long tail of inactive followers dominates.
- Geo-aware fanout. Push to the celebrity’s regional followers first, others later. Helps perceived freshness.
Twitter has used variants of (1) and (3) — pure pull for the highest-follower accounts, active-subset push for the next tier, full push for the rest.
8.3 Merging Push and Pull
Naive merge: collect all candidates, sort by tweet_id, take top N. For a user’s home timeline:
- 50 from inbox (push-delivered).
- 5 celebrities × 10 recent tweets each = 50 from pull.
Merge of 100 items, sort, take 50. Fast — microseconds in memory.
The trickier issue: the inbox is capped at 800 entries. A user who hasn’t read their timeline in a week may have missed tweets that have since fallen off the cap. The “deep timeline” — older than what the inbox holds — requires a separate read path that pulls from the tweet store directly. This is acceptable because deep timelines are rare reads; most users’ timeline session reads the recent few hundred tweets.
8.4 Snowflake ID Generation
Twitter’s Snowflake (announced on the Twitter engineering blog in 2010) generates 64-bit IDs locally on each ID-generator process with no central coordinator. The exact layout is fixed by the original Scala source (IdWorker.scala in twitter-archive/snowflake), and it is worth getting right because secondary write-ups routinely collapse the two machine-ID fields into one “10-bit machine ID”:
[1 bit unused (sign) | 41 bits timestamp ms | 5 bits datacenter ID | 5 bits worker ID | 12 bits sequence]
- The high bit is unused so the ID is always a positive signed 64-bit integer.
- 41 bits of millisecond timestamp, counted from a custom epoch (
twepoch = 1288834974657ms ≈ 2010-11-04), gives ~69 years of unique IDs (2⁴¹ ms ≈ 69.7 years) before the field wraps. - The machine identifier is two 5-bit fields, not a single 10-bit one:
datacenterIdBits = 5andworkerIdBits = 5, withmaxDatacenterId = maxWorkerId = 31(the source computes these as-1 ^ (-1 << 5)). That is 32 datacenters × 32 workers = 1024 distinct generators in aggregate — which is why the “10-bit machine ID” shorthand gives the right count even though it misnames the structure. - 12 bits sequence supports 4096 (2¹²) IDs per generator per millisecond — about 4 million IDs/s/generator. When the sequence overflows within a millisecond, the worker spins until the clock advances.
Each generator produces IDs without contacting any other server — eliminating the central-counter contention point. Because the timestamp occupies the most-significant bits, IDs are roughly time-sortable globally (within a single millisecond, only the local sequence breaks ties, so two near-simultaneous tweets from different generators may sort by an arbitrary-but-stable order), which is sufficient for “show recent tweets first” purposes. The bit-layout constants above are taken directly from the original IdWorker.scala, the primary source.
8.5 Backfill on New Follow
When user A follows user B, A’s timeline should start including B’s tweets. Two options:
- Lazy. Only B’s future tweets appear. A’s existing inbox is unchanged. Simple; user accepts that following someone won’t retroactively populate. Twitter has used this for the most part.
- Backfill. Asynchronously read B’s last K tweets and inject them into A’s inbox. Costlier but more user-friendly.
Lazy is the typical answer; backfill is an optional follow-up.
8.6 Ranked Timeline (Post-2016)
Twitter introduced a ranked home timeline circa 2016 — instead of strict chronology, tweets are scored on relevance (engagement model) and the top-N from a recent window are surfaced. Implementation:
- The same materialized inbox (or an extended candidate set from inbox + pull) feeds a ranking model.
- The ranking model scores each candidate tweet and selects/orders.
- Ranking can be server-side at read time (incurs ML latency) or pre-ranked at write time (incurs storage cost and stale relevance scores).
Twitter has publicly described running the ranker at read time, with caching of recent rankings to amortize cost across multiple reads in a session.
9. Scaling Strategy
9.1 What Breaks First
- Fanout queue lag. A celebrity’s tweet backs up the fanout pipeline. Solved by celebrity pull-path.
- Inbox write contention. Active users have many concurrent inbox writes from fanout. Solved by sharding inboxes (consistent hashing on user_id).
- Read amplification on tweet hydration. A timeline page is 50 tweets × engagement counters × media URLs. Caching tweet objects by ID is essential.
- Search index ingestion lag. Tweets must be searchable within seconds; Earlybird’s design (real-time inverted index, in-memory segments) is purpose-built for this.
9.2 Sharding
- Tweets: consistent hashing (see Consistent Hashing) on
tweet_id. Random distribution; no hot shards. - Inboxes: consistent hashing on
user_id. Hot users’ inboxes can become hot shards; mitigations include splitting a hot user’s inbox across multiple shards. - Social graph:
followingis small per user (sharded by user_id).followersfor celebrities is huge — partitioned across multiple shards by hash(user_id, follower_id_chunk).
9.3 Geo-Replication
Multi-region replication of tweet store and social graph. Inboxes are typically region-local: a user reads from their home region’s inbox; cross-region tweet propagation happens via the global Kafka stream, with each region’s fanout service writing to the local inbox tier.
10. Real-World Example
Twitter (now X). The architecture as described in Krikorian’s 2013 QCon talk and several engineering blog posts:
- Tweet storage. Originally MySQL (sharded). Migrated to Manhattan, Twitter’s homegrown distributed multi-tenant key-value store, in 2014 (Twitter engineering blog, 2014). Manhattan’s partitioning is consistent-hash-based, with quorum semantics and regional replicas.
- Inbox storage (timeline). Redis Cluster, sharded by user_id, sorted-set datatype.
- Social graph. Twitter’s homegrown FlockDB (graph store) historically; later replaced by a purpose-built graph layer over Manhattan.
- Fanout. Custom service consuming from Kafka, parallelizing inbox writes.
- Search. Earlybird (Busch et al. ICDE 2012) — a real-time inverted index built on a single-writer/multiple-reader concurrency model with memory barriers; each Earlybird instance manages multiple index segments (12 at the time of the paper), only one of which is actively being written while the rest are read-only. The paper states tweets are “searchable within 10 seconds after creation” and that Earlybird served “over two billion queries a day with an average query latency of 50 ms” (Busch et al., Earlybird: Real-Time Search at Twitter, ICDE 2012). Krikorian’s talk adds that search reads are “on the order of 100 msecs” and “search never hits disk” (High Scalability).
- ID generation. Snowflake (Twitter engineering blog, 2010), described in §8.4.
- Ranking. Read-time ML model serving (post-2016), with a separate model-serving infrastructure.
Krikorian’s celebrated 2013 talk emphasized that the celebrity problem was the defining technical challenge — and the hybrid push/pull architecture was Twitter’s answer.
Uncertain
Verify: that Manhattan is still Twitter/X’s primary tweet store today. Reason: the published architecture is the 2013–2018 design (the 2014 “Manhattan” engineering-blog post is the primary source for its introduction, but it predates the 2022 acquisition); X stopped publishing infrastructure write-ups after 2022, so post-acquisition stack changes are undisclosed. To resolve: a current X/Twitter engineering disclosure or a credible insider account of the present tweet-storage layer. The 2013–2018 design above remains correct as of its era. uncertain
11. Tradeoffs
| Design Choice | Option A | Option B | When A wins | When B wins |
|---|---|---|---|---|
| Fanout strategy | Pure push | Pure pull | Few followers, simple model | Few users, many followed accounts |
| Fanout strategy | Hybrid push/pull | Pure approach | Huge variance in follower counts (Twitter case) | Homogeneous user base |
| Inbox cap | Short (100s) | Long (10000s) | Most users read recent only | Catch-up reads important |
| Backfill on follow | Lazy | Eager backfill | Cost-bound, simple | UX-prioritized |
| Tweet ordering | Strict chronological | Relevance-ranked | Predictable, transparent | Higher engagement, accept opacity |
| Tweet store | RDBMS (MySQL) | KV (Manhattan/Cassandra) | < 10B tweets, query flexibility | > 100B tweets, simple key access |
| ID generation | Centralized counter | Snowflake | Need strict global order | Need decentralized, time-ordered |
| Search | Built-in | External (Elasticsearch) | Small scale | Real-time, full-text |
| Engagement counters | Sync UPDATE | Async aggregation | Low volume | High volume, eventual consistency OK |
12. Pitfalls
-
Designing pure fanout-on-write without considering celebrities. Falls apart at the first whale account. Always cover the hybrid in the interview answer.
-
Computing fanout synchronously with the tweet write. The user waits until N inbox writes complete — terrible latency. Always async via event stream.
-
Forgetting that the inbox is capped. A user who hasn’t read their timeline in 30 days might need to fetch from “deep timeline” (the tweet store directly, not their inbox). Without this fallback, deep history is invisible.
-
Sorting timeline by
created_atfrom the row. The Snowflake ID already encodes time; sorting by ID is faster (integer sort) and consistent. Plus, two tweets with the samecreated_atneed a tiebreaker — Snowflake’s local sequence provides one. -
Treating retweets as plain tweets. A retweet’s “ranking” should reflect the original tweet’s age, not the retweet event’s age. The schema needs a
retweet_of_idfield and the ranking logic must dereference it. -
Inconsistent celebrity threshold across services. If the fanout service thinks user X is a celebrity but the read service thinks they’re normal (and reads only from inbox), tweets disappear. The threshold must be authoritative and consistent — typically a flag on the user’s profile, updated atomically.
-
Naive follower-list fetch during fanout. A celebrity’s
followersrow is GBs; loading it in memory is impossible. Fanout must stream the follower list in chunks. -
Search index lag visible to users. If “search this tweet” returns nothing for 60 seconds after posting, users notice. Earlybird’s < 10s target reflects this.
-
Engagement counter race on sync UPDATE. A viral tweet’s like count receives 100K likes/min — naive UPDATE locks the row. Same fix as Reddit votes: async aggregation. See Distributed Counter System Design.
-
Cross-region inbox inconsistency. If user posts in region A and follower lives in region B, the tweet must replicate to region B’s inbox. Async replication causes “I just tweeted but my friend doesn’t see it” complaints — a window of seconds is acceptable; minutes is not.
-
Spam tweet propagating to millions of inboxes before detection. Spam detection at write time, not just at read time — once it’s in 1M inboxes, you can’t easily yank it.
-
Forgetting that a follow itself is a write event. A follow triggers a graph write and a backfill (if eager) — both must be idempotent for retry safety.
13. Common Interview Variants and Follow-Ups
- “Now add tweet ranking (relevance instead of chronological).” Layer an ML scorer at read time on the merged timeline; cache rankings for a session window.
- “Now support quote tweets and conversations.” Quote tweets are a tweet-with-embedded-tweet — a foreign key. Conversations require fetching the whole reply chain — a recursive fetch up to the root tweet.
- “Build the search subsystem.” That’s Twitter Search System Design — Earlybird-style real-time inverted index. Distinct from this note.
- “Build the trending topics.” Heavy hitters detection via Count-Min Sketch or HyperLogLog on the tweet stream, with time decay.
- “How does the read path handle a user following 5000 accounts?” Their inbox handles all of them via push (since most are not celebrities); merge cost stays bounded.
- “Add private accounts (only approved followers see tweets).” A new visibility column on the tweet; fanout filters by the author’s privacy setting. Tweets must not appear in non-follower searches.
- “Build the ‘who to follow’ recommendation.” Collaborative filtering on the social graph — entirely separate from timeline serving; consumes the same data but offline.
- “How do you handle deletion of a tweet that already fanned out?” Mark
is_deleted=truein tweet store; rendering filters deleted tweets at read time. Inbox entries pointing at deleted tweets become broken on render — accepted because deletes are rare. - “Make it support 10× the scale.” Scale primarily by sharding; the hybrid fanout already scales linearly with users so 10× users = 10× shards. The harder constraint is the long-tail celebrity problem getting more extreme.
14. Open Questions / Uncertain
Two earlier flags in this note have been resolved against primary/near-primary sources and are no longer open:
- The 800-entry home-timeline cap and 3× replication are confirmed verbatim from Krikorian’s Timelines at Scale talk (transcribed by High Scalability) — see §5.2.
- The hybrid push/pull design (pull/merge-at-read for the highest-follower accounts) is confirmed by the same talk’s “for people like Taylor Swift … merge in her timeline at read time” — see §8.1.3. The talk frames the celebrity boundary qualitatively; Twitter has never published a numeric follower threshold, so the absence of a published cutoff is itself the verified finding, not an open uncertainty.
The following remain genuinely open:
Uncertain
Verify: that Manhattan is still X/Twitter’s canonical tweet store post-2022. Reason: X ceased publishing infrastructure write-ups after the acquisition; the primary “Manhattan” source dates to 2014. To resolve: a current engineering disclosure. uncertain
Uncertain
Verify: the specifics of the post-2016 ranked-timeline model (features, model class, serving topology). Reason: Twitter ML blog posts and the 2023 “the-algorithm” open-source release describe it at a high level, but the production feature set and exact serving path are proprietary/partial. To resolve: read the open-sourced
the-algorithmrepository and any current ranking write-ups end to end. uncertain
15. See Also
- Major System Designs MOC
- SWE Interview Preparation MOC
- Instagram Photo Sharing System Design — sibling fanout system with photos
- Reddit Forum System Design — sibling content-feed system with different ranking
- Twitter Search System Design — Earlybird real-time inverted index
- Notification Service System Design — reply / mention / DM notifications
- Distributed Counter System Design — likes, retweets, reply counters
- Consistent Hashing — for sharding tweet store, inbox tier, and graph
- Distributed Log System Design — Kafka, used for the write-event stream
- Inverted Index — the data structure underneath Earlybird
- Bloom Filter — for “have we shown this tweet before” deduplication in ranked timelines
- Top K Trending System Design — for trending topics
- LRU Cache — for tweet hydration cache
- Recommendation Engine System Design — for the ranked-timeline model serving