~learn.

Twitter/X, the whole design.

A timeline system that puts ~500 million posts a day into followers' home timelines within 5 seconds and serves ~300,000 timeline reads a second from memory, by pushing ordinary posts at write time and pulling celebrity posts at read time.

7 diagrams · 8 sections · revise in ~10 min · Download PDF prints it in the light theme, diagrams included

SQ-10 · Twitter/X · design sheethimanshukukreja.in/learn/sheets/bonus-10-twitter-x

At a glance#

Number Value What it drives
Posts a day ~500 million ~6,000 / s average, ~60,000 / s at a 10× event peak
Timeline reads ~300,000 / s 50 : 1 reads to writes, so precompute timelines
Fan-out writes ~4.2 million / s 6,000 posts × ~700 followers, queue + workers
Biggest accounts 100M+ followers one push = 100M inserts, ~17 min at 100K / s
Latency targets < 5 s delivery · < 200 ms read post confirms in < 500 ms, fan-out is async
Timeline cache 800 IDs × ~20 B = 16 KB / user 250M users ≈ 4 TB, 12 TB with 3 replicas
Tweet storage ~1 KB each · 500 GB / day ~180 TB a year, hot and cold tiers
Social graph ~420 billion edges stored both directions, ~7 TB of IDs

Architecture#

DiagramTwitter on one page: post, fan out, read and merge
%% title: Twitter on one page: post, fan out, read and merge
flowchart TD
  APP@{ icon: "sd:phone", label: "Clients<br/>web · iOS · Android" } --> GW@{ icon: "sd:gateway", label: "API gateway / LB<br/>rate limit · auth · routing" }
  GW ==>|"post"| TW["Tweet service<br/>store first · confirm < 500 ms"]:::core
  GW ==>|"read"| TL["Timeline service<br/>read list · merge celebrities · hydrate"]:::core
  TW --> TST@{ icon: "sd:database", label: "Tweet store · Manhattan<br/>by tweet_id · source of truth" }
  TW -->|"author > 10K followers"| CT@{ icon: "tech:redis", label: "Redis · celebrity_tweets<br/>last 100 per author" }
  TW --> K@{ icon: "tech:kafka", label: "Kafka<br/>fan-out jobs" }
  K ==> FO["Fan-out workers<br/>batches of 1,000 followers"]:::hot
  K --> ES@{ icon: "tech:elasticsearch", label: "Elasticsearch<br/>search · trends" }
  FO --> SG@{ icon: "sd:users", label: "Social graph<br/>followers + following lists" }
  FO ==>|"LPUSH + LTRIM 800"| TC@{ icon: "tech:redis", label: "Redis timeline cache<br/>by user_id · 3 replicas" }
  TL --> TC
  TL --> CT
  TL -.-> TST
  class APP client
  class GW edge
  class TST,SG,ES store
  class TC,CT cache
  class K queue
DiagramFan-out service: push or pull, decided per author
%% title: Fan-out service: push or pull, decided per author
flowchart TD
  P["New tweet stored"]:::core --> D{"Author has > 10K followers?"}
  D -->|"no · push"| Q@{ icon: "tech:kafka", label: "Kafka<br/>one job per 1,000 followers" }
  Q --> W["Fan-out workers<br/>scale on queue depth"]:::hot
  W --> L@{ icon: "tech:redis", label: "timeline:follower<br/>LPUSH id · LTRIM to 800" }
  D -->|"yes · pull"| C@{ icon: "tech:redis", label: "celebrity_tweets:author<br/>ZADD by time · keep 100" }
  C --> R["Merged into each follower's<br/>timeline at read time"]:::service
  class Q queue
  class L,C cache

Key flows#

DiagramPost: store, confirm, fan out in the background
%% title: Post: store, confirm, fan out in the background
sequenceDiagram
  autonumber
  participant A as Author
  participant T as Tweet service
  participant S as Tweet store
  participant K as Kafka
  participant W as Fan-out workers
  participant G as Social graph
  participant C as Timeline cache
  A->>T: Post tweet
  T->>S: Store tweet (source of truth)
  T-->>A: Posted · under 500 ms
  T->>G: Follower count?
  G-->>T: 1,000 · under 10K, so push
  T->>K: Enqueue fan-out batch
  K->>W: Batch: tweet ID + 1,000 follower IDs
  W->>C: LPUSH timeline:f1..f1000 · LTRIM 800
  Note over A,C: in followers' timelines within 5 s · a celebrity would skip steps 6-8
DiagramHome timeline read: merge, hydrate, rebuild on a miss
%% title: Home timeline read: merge, hydrate, rebuild on a miss
sequenceDiagram
  autonumber
  participant U as User
  participant TL as Timeline service
  participant C as Timeline cache
  participant G as Social graph
  participant CT as Celebrity lists
  participant S as Tweet store
  U->>TL: Get home timeline
  TL->>C: LRANGE timeline:user 0 799
  alt cache hit
    C-->>TL: pushed tweet IDs
  else evicted · user back after 30+ days
    TL->>S: recent tweets of followed non-celebrities
    TL->>C: rebuild list · TTL 30 days
  end
  TL->>G: followed accounts with > 10K followers
  TL->>CT: ZREVRANGE each, top 20
  Note over TL: merge by time · dedupe · top N
  TL->>S: MGET tweet bodies + authors
  Note over TL: drop deleted, unfollowed, blocked
  TL-->>U: Timeline · under 200 ms p99
DiagramDelete 30 s after posting: tombstone, don't scrub
%% title: Delete 30 s after posting: tombstone, don't scrub
sequenceDiagram
  autonumber
  participant A as Author
  participant T as Tweet service
  participant S as Tweet store
  participant X as Caches · search · trends
  participant R as Timeline reads
  A->>T: Delete tweet
  T->>S: Mark deleted (tombstone)
  T->>X: Evict body · remove from index · stop counting
  T-->>A: Deleted
  R->>S: Hydrate IDs, including the deleted one
  S-->>R: tombstoned · drop it
  Note over R: gone from every timeline at once · stale IDs age out past 800

Data model#

DiagramCore entities
%% title: Core entities
classDiagram
  class Tweet {
    +tweetId : id · partition key
    +authorId : id
    +content : 280 chars · media optional
    +parentTweetId : id · for replies
    +createdAt : timestamp
    +deleted : bool · tombstone
    +likeCount : sharded counter
  }
  class User {
    +userId : id
    +followerCount : counter
    +followingCount : counter
    +isCelebrity : followers over 10K
  }
  class Follow {
    +followerId : id
    +followeeId : id
    +createdAt : timestamp
  }
  class HomeTimeline {
    +key : timeline:userId
    +entries : tweetId · authorId · ts
    +maxSize : 800 · about 16 KB
    +ttl : 30 days idle
  }
  class CelebrityTweets {
    +key : celebrity_tweets:authorId
    +entries : sorted set by timestamp
    +maxSize : 100
  }
  User "1" --> "many" Tweet : posts
  User "1" --> "many" Follow : follows / followed by
  User "1" --> "1" HomeTimeline : reads
  HomeTimeline "many" --> "many" Tweet : holds IDs of
  User "1" --> "0..1" CelebrityTweets : if over 10K

Lifecycle#

DiagramA tweet's life in timelines
%% title: A tweet's life in timelines
stateDiagram-v2
  [*] --> STORED: author posts
  STORED --> FANNING_OUT: up to 10K followers
  STORED --> PULL_LISTED: over 10K followers
  FANNING_OUT --> DELIVERED: all batches written
  FANNING_OUT --> DELAYED: queue backlog
  DELAYED --> DELIVERED: workers catch up
  PULL_LISTED --> DELIVERED: merged at read
  DELIVERED --> DELETED: tombstone
  DELIVERED --> AGED_OUT: past 800 entries
  AGED_OUT --> [*]
  DELETED --> [*]
  class DELIVERED,AGED_OUT ok
  class DELETED bad
  class DELAYED queue

Decisions#

Decision Chose Over Because
Building timelines Push up to 10K followers, pull above Pure push or pure pull fast reads without 100M writes per celebrity post
Cache contents ~20 B tweet IDs ~1 KB full tweets 4 TB instead of ~200 TB; hydrate on read
Fan-out timing Kafka jobs, 1,000 followers each Inline in the post request confirm in < 500 ms; workers scale on depth
Cache partitioning By user_id, 3 replicas By tweet_id one timeline read hits one shard
Follow storage Edge stored in both directions One table, one index fan-out and reads each need one side
Consistency Eventual for timelines, strong for tweets Strong everywhere a few seconds' lag is fine; author sees own post
Delete and unfollow Filter at hydration Scrub every timeline scrubbing is a fan-out as costly as posting

When it breaks#

Failure What users see Contain it with
Event spike, 10× posts posts arrive 10–30 s late pre-scale from the event calendar; reads first, stale is fine
Celebrity lists become hot keys merge 25 → 150 ms, slower feed replicate those keys across more nodes
Returning user, timeline evicted slower first load rebuild from followed accounts, cache again
Cache hit rate < 95 % slow reads, stores overloaded critical alert; each miss is a full rebuild
Viral tweet's like counter counts lag a little shard the counter, sum on read
Reply lands before its parent reply without context parent_tweet_id, fetch the parent at read

Say it in the interview#