30-Second Glance
- What: X/Twitter timelines — home (ranked, hybrid fan-out) + user (per-author, pull) — text-first with media/graph side-paths.
- Scale: ~500M tweets/day (~6k/s avg, 25k/s peak events); fan-out ≈ 100B deliveries/day; home-timeline cache = top ~800 ids/user in Redis.
- Core trick: Reuses News Feed case hybrid fan-out, plus celebrity outboxes, early-ranked ingestion, and separate media + graph stores.
- Hardest part: Peak-event write storms (World Cup final); home vs user timeline divergence; mixing ads/who-to-follow; media upload not blocking tweet-accept.
Interactive playground Push vs pull fan-out with a World-Cup-final storm button: Open Twitter Timeline simulator →
1. Requirements
Functional:
- Post tweet (280 chars + up to 4 media refs + poll/quote refs).
GET /home-timeline— ranked mix of followed authors, ads, who-to-follow (cursor pages).GET /user-timeline/{user}— reverse-chron author archive (public, CDN-cacheable).- Follow/unfollow, like, retweet/repost, reply threading.
- Delete cascades from timelines within ~1 min.
Non-functional:
| Requirement | Target |
|---|---|
| Home timeline p99 | < 200 ms |
| Tweet-accept p99 | < 300 ms (media processed async — accept must not wait for transcode) |
| Post-to-home visibility | < 5 s normal, < 30 s via celebrity pull |
| Availability | 99.95% reads; reads degrade (stale) before they fail |
Out of scope:
- Full-text/keyword search index (separate search stack).
- DMs, Spaces/live audio, long-form articles.
- Counter-abuse / spam classifiers beyond hooks.
2. Capacity Estimation
Assumptions: 400M users, 200M DAU; 500M tweets/day; avg 200 followers; 20% tweets carry media (avg 1 MB); peak 4× (sport finals); home-timeline cache 800 ids × 64 B ≈ 50 KB/user.
| Metric | Math | Result |
|---|---|---|
| Tweet writes (avg / peak) | 500M / 86400 ≈ 5.8k | ~6k / ~25k |
| Fan-out deliveries (pure push) | 500M × 200 / 86400 | ~1.2M/s avg — the forcing function for hybrid fan-out |
| Home reads | 200M × 10 opens / 86400 | ~23k/s avg, ~70k/s peak |
| Home-cache RAM (active) | 80M cached users × 50 KB | ~4 TB Redis Cluster (LRU + 7 d TTL; dormant users rebuilt on demand) |
| Media ingress | 100M/day × 1 MB | ~100 TB/day → object storage + CDN (see §6.2) |
3. API Design
POST /api/v2/tweets
{ "text": "...", "media_ids": ["m_1","m_2"],
"reply_to": null, "quote_of": "t_88" }
201 { "tweet_id": "t_71xQ2", "created_at": "...", "fanout": "pending" }
GET /api/v2/home-timeline?limit=20&cursor=...
200 { "items": [{ "tweet_id": "t_71xQ2", "text": "...", "author": {...},
"entities": {"media": [...], "poll": null},
"like_count": 1204, "rank_reason": "followed:elonmusk" }],
"next_cursor": "..." }
GET /api/v2/users/{id}/tweets?limit=20&cursor=... // user timeline, CDN-cacheable
POST /api/v2/tweets/{id}/like | /retweet | /delete // 204; counts via Kafka| Endpoint | Purpose | Notes |
|---|---|---|
POST /api/v2/tweets |
Accept tweet | Validates + persists + enqueues; never waits on media pipeline |
| home-timeline | Ranked mix | private, max-age=15s; cursor (score,id); injects ads + WTF slots server-side |
| user-timeline | Author archive | public, max-age=60s at CDN — pull path, cheap |
| media upload | POST /api/v2/media/upload → presigned URL |
Direct-to-object-store; tweet refs media_id after PROCESSING→READY (or accept with blurhash placeholder) |
4. Data Model & Storage
| Entity | Store | Partition / shard key | Why this store |
|---|---|---|---|
| Tweet bodies | Cassandra / Manhattan-like wide-column | tweet_id (time-ordered) | Immutable, write-heavy, point reads |
| Home timelines (id lists) | Redis zset, cap ~800 | user_id | Same pattern as News Feed L1 |
| User timelines | Cassandra tweets_by_author + Redis head-cache |
author_id | Reverse-chron range scans per author |
| Follow graph | FlockDB-style adjacency (sharded MySQL) | user_id | Edge queries both directions, huge fan-out reads |
| Media objects | Object storage + CDN | media_id | Bytes off the transactional path |
| Counters (likes/RTs) | Redis hot → Cassandra durable | tweet_id | Approximate, loss-tolerant |
-- tweets_by_id (tweet_id TIMEUUID PK, author_id, text, media_ids, reply_to, created_at)
-- tweets_by_author ((author_id), created_at DESC, tweet_id) -- user-timeline range scan
-- home:{user_id} ZSET score=rank member=tweet_id, MAXLEN 800
-- celebrity outbox: celeb:{author_id} LIST (capped 3000)- Bodies immutable except delete (tombstone → async timeline scrubber removes ids within ~1 min).
- Retweet = new tweet row with
retweet_ofpointer (keeps author-timeline semantics clean; dedup at rank time).
5. High-Level Design
Write path: tweet → API → tweet store → fan-out decider → normal: Kafka → workers push to followers' home zsets (+ author timeline row) / celebrity: celebrity-outbox only. Read path: home-timeline → Redis zset → hydrate (tweet-cache → Cassandra) → merge celebrity outboxes + ads + WTF → rank/filter → page. User-timeline: CDN → author head-cache → tweets_by_author scan (pure pull, no fan-out).
- Ingest service: auth, validation, dedup (client mutation-id), persist, enqueue — p99 budget owned here.
- Fan-out service: the News Feed hybrid, sharded by follower-range; early-rank pre-scores at write time (author affinity buckets) so reads merge cheaply.
- Timeline service (read): zset paging + hydration + mixing; owns cursor codec and seen-filter.
- Media pipeline: upload → transcode/thumbnail/NSFW → CDN (async; tweet renders progressively).
- Graph service: follow edges + WTF candidate generation input.
6. Deep Dives
6.1 Hybrid fan-out at Twitter scale + peak events
Directly extends News Feed case §6.1 with two Twitter-specific twists. (1) Threshold is dynamic: follower-count cutoff auto-lowers when fan-out lag exceeds SLA (a mid-size account going viral mid-match gets promoted to pull-path within minutes). (2) Early ranking at ingest: each pushed id carries a precomputed partial score (author-affinity bucket, media boost, language match); read-time ranker merges rather than scores from scratch — keeps the 200 ms p99 while merging 3 sources + ads. World-Cup-final playbook: pre-lower thresholds, pre-scale workers on lag (not CPU), shed WTF/ads mixing first (timelines stay correct, just less monetised), serve stale-while-revalidate past 30 s lag.
6.2 Media and graph objects (the non-text half)
- Media objects: client uploads bytes direct to object storage via presigned URL (origin never proxies bytes); async workers produce variants (thumb/small/large, video HLS renditions) + blurhash placeholder + NSFW/safety labels; tweet row references
media_idswith per-variant CDN URLs. Tweet-accept only waits for the metadata row, never transcode — media renders progressively. Hot media (viral video) is CDN-cached at edge; origin shield + signed URLs for sensitive content. - Graph objects: follow edges in a dedicated adjacency store (FlockDB-style: sharded by
(user_id, edge_type)), because fan-out reads follower lists at 1M+/s and a general RDBMS buckles. Forward edges (who I follow) feed the pull path; reverse edges (my followers) feed push sharding. WTF and ads candidates come from graph walks (2-hop) + offline embeddings, injected at mix time — which is why they're the first thing shed under load (§6.1).
6.3 Home vs user timeline: why two systems
| Home timeline | User timeline | |
|---|---|---|
| Query | "best of my graph" (ranked, mixed) | "everything by author X" (chronological) |
| Fan-out | Hybrid push + celebrity pull | None — pure pull from tweets_by_author |
| Cache | Per-viewer Redis zset (private) | Per-author head-cache + CDN (public, shared) |
| Cost shape | Viewer × sources | Author × readers (CDN absorbs) |
Consequence: profile views of a celebrity cost ~zero origin (CDN), while home timelines of their followers cost a bounded merge. Deletes: tombstone in tweet store → scrubber removes id from home zsets (sampled scan, ~1 min) + purge CDN user-timeline key. Say in the interview: "user timeline is a cacheable archive; home timeline is a per-viewer computation — different SLOs, different stores."
7. Bottlenecks, Trade-offs & Monitoring
First bottleneck at 10x: fan-out worker lag during global events → dynamic celebrity thresholds + lag-based autoscaling + shed mixing (ads/WTF) before correctness. Next: home-zset Redis memory → 800→400 cap + 7 d→3 d TTL for low-activity users. Next: graph-store reverse-edge reads for mega-accounts → precomputed follower-shard snapshots.
| Approach | Pros | Cons | Use when |
|---|---|---|---|
| Pure push | Simplest reads | 1.2M/s delivery storms | Never at this scale |
| Hybrid + early rank (chosen) | Bounded writes, fast merged reads | Two paths + precompute pipeline | Production |
| Media-via-origin | Simple | Origin bandwidth death | Prototype only |
| Presigned direct upload (chosen) | Scales infinitely, cheap | Async consistency (placeholder UX) | Production |
| Metric | Alert if |
|---|---|
| Home p99 | > 300 ms for 5 min |
| Fan-out lag | > 60 s (dynamic-threshold trigger arms at 30 s) |
| Tweet-accept p99 | > 500 ms (ingest backing up — page) |
| Celebrity-merge p99 | > 100 ms |
| Media READY lag (upload→variants) | > 2 min p50 |
Interview one-liners
- Q: How is this different from News Feed? / A: Same hybrid core, plus dynamic celebrity thresholds, early-rank at ingest, and split home (per-viewer compute) vs user (cacheable archive) timelines.
- Q: World Cup final — what breaks first? / A: Fan-out lag; playbook is lower thresholds, lag-based worker scale, shed ads/WTF mixing first.
- Q: Why doesn't tweet-post wait for video transcode? / A: Accept persists metadata + enqueues; media renders progressively from blurhash to variants.
Related
- News Feed case — prerequisite; hybrid fan-out, ranking, cache tiers
- Instagram case — media pipeline sibling (heavier video/photo focus)
- Cache Patterns and Eviction — zset + CDN layering, stampede guards
- Partitioning — timeline/follower-range sharding
- Stream Processing — fan-out queue, counters, early-rank stream
- MongoDB vs Cassandra — tweet store choice
- Kafka vs RabbitMQ — fan-out transport choice
try it live
Steppable request flow, capacity sliders and failure toggles — open the interactive simulator →