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- Writes pay, reads are cheap. At 50 : 1, doing the work once per post makes each of the 300K reads a cache lookup.
- The timeline cache is the serving layer. It holds only IDs and can always be rebuilt from the tweet store.
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 cacheKey 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 p99DiagramDelete 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- Timelines partition by
user_idhash (~10M users a shard, 3 replicas); tweets partition bytweet_id. - Each follow is written twice,
followers:{id}andfollowing:{id}, so fan-out and reads are each one lookup.
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#
- "Push for most, pull for celebrities." Push or pull is decided per author by follower count, so a viral account flips to pull for its next posts.
- Do the math out loud: 6,000 × 700 = 4.2M writes a second is fine; 100M ÷ 100K/s ≈ 17 minutes for one celebrity post is not.
- Cache IDs, not tweets: 800 × 20 B × 250M users ≈ 4 TB, so every active timeline fits in memory.
- The cache is the serving layer; the tweet store is the truth, and any timeline can be rebuilt from it.
- Confirm after the store write, fan out after: the author waits < 500 ms, followers get it within 5 s.
- Delete with a tombstone: hydration drops it everywhere at once; stale IDs age out past 800.
- Degrade by delay, not failure: in a spike, keep reads at 99.99 % and let delivery slip to 10–30 s.