Design Twitter
Design Twitter (now X) at 250M DAU, 500M tweets/day. Covers the celebrity fan-out problem (Elon's tweet to 150M followers), hybrid fan-out-on-write with celebrity bypass, Snowflake IDs for time-sortable globally-unique tweet IDs, Cassandra for tweet storage, Redis ZSET timeline caches, and Elasticsearch search fed asynchronously by Kafka.
Foundational.
How it works
What are we designing? Twitter (now X). Users post short text messages (tweets, up to 280 chars), optionally with images or video. They follow other users and see a reverse-chronological (or ranked) timeline of tweets from people they follow. They can retweet, quote-tweet, reply, and like. The defining challenge is the same as Instagram's — fan-out — but at much higher text volume (tweets are tiny, so users post far more often than photos), with the added complication of the 'celebrity problem' at extreme scale (a single tweet from Elon Musk reaches 150M+ followers instantly).
Functional requirements.
- Post a tweet (text + optional media).
- Follow / unfollow users.
- Browse the home timeline (tweets from followed users).
- Browse the user timeline (a specific user's tweets).
- Retweet, quote, reply, like.
- Search tweets (full-text).
Non-functional requirements.
- Timeline read latency: p99 < 100 ms (Twitter's actual SLO).
- Tweet post latency: < 500 ms until the tweet is visible to the poster.
- Availability: 99.99% (Twitter outages make global headlines).
- Scale: 250M DAU, 500M tweets/day, 100B+ timeline reads/day.
- Search: < 500 ms for full-text search across the entire tweet corpus.
Non-goals. No DM (separate system), no Moments, no algorithmic ranking in v1.
Capacity estimation.
Tweets. 500M tweets/day, each ~300 bytes (text + metadata, media stored separately). 500M x 300 B = 150 GB/day of tweet text = ~55 GB/year. Tiny by storage standards; a single Cassandra node could hold years of text. With media (10% of tweets have images, avg 500KB), 50M x 500KB = 25 TB/day of media.
Timeline reads. 250M DAU x ~50 refreshes/day = 12.5B timeline reads/day = ~145K reads/sec average, peak ~700K/sec.
Fan-out. Average user follows ~200 people and is followed by ~200. 500M tweets/day x 200 = 100B fan-out writes/day = 1.15M writes/sec. This is the real load: writes, not reads, because of fan-out-on-write.
Celebrity fan-out. Elon Musk (150M followers) tweeting once = 150M fan-out writes. At Twitter's peak, ~20 such celebrity tweets/day = 3B writes/day = ~35K writes/sec just from celebrities. This is why celebrities need pull-on-read.
Storage. Timeline cache: 250M users x 800 tweet_ids (last 3 days) x 8 bytes = 1.6 TB in Redis. Fits in a medium Redis cluster.
Bandwidth. 145K reads/sec x 5KB (response = 20 tweets x 250 bytes) = 725 MB/s = 5.8 Gbps egress. With media served from CDN, origin egress is mostly text.
APIs.
POST /v1/tweets (text, media_ids[])
GET /v1/tweets/:id
DELETE /v1/tweets/:id
POST /v1/tweets/:id/retweet
POST /v1/tweets/:id/like
GET /v1/timeline/home?cursor=... -> reverse-chrono tweets from followed users
GET /v1/timeline/user/:id?cursor -> a specific user's tweets
POST /v1/users/:id/follow
DELETE /v1/users/:id/follow
GET /v1/search?q=...&type=latest -> full-text searchTweets are written via a single POST /tweets. The service inserts the tweet into the store and fires a fan-out job asynchronously so the poster's request returns immediately.
Data model.
Tweets (Cassandra, partitioned by tweet_id, replicated RF=3):
tweets (
tweet_id TIMEUUID, -- Snowflake ID, sortable by time
user_id BIGINT,
text TEXT,
media_urls LIST<TEXT>,
reply_to BIGINT NULL,
retweet_of BIGINT NULL,
created_at TIMESTAMP,
PRIMARY KEY (tweet_id)
)Partitioned by tweet_id (Snowflake). A secondary index by user_id supports user-timeline queries.
Follows (Cassandra, partitioned both ways):
follows (
follower_id BIGINT,
followee_id BIGINT,
created_at TIMESTAMP,
PRIMARY KEY (follower_id, followee_id)
)
followers_by_followee (followee_id, follower_id, ...) -- materialized inverseTimeline cache (Redis, sorted set per user):
key: tl:{user_id}
value: ZSET member=tweet_id, score=created_at_timestamp
capped at 800 entries (last ~3 days)Tweet IDs are Snowflake: 64 bits = 41-bit millisecond timestamp + 10-bit worker_id + 12-bit sequence. Globally unique, sortable by time, no coordinator.
Deep dive: the celebrity problem and hybrid fan-out.
Twitter is the system that invented the celebrity-fan-out problem in production. The naive fan-out-on-write model says: when a user tweets, push the tweet_id into every follower's timeline cache. For a normal user (avg 200 followers) this is 200 writes. For Lady Gaga (80M followers) it's 80M writes per tweet. With ~30 celebrity tweets/day at that scale, you spend more writes on celebrities than on the entire rest of the user base.
Twitter's solution (and ours). Detect celebrity accounts at the write path (>10K followers) and SKIP fan-out for them. Their tweets stay only in their own user-timeline shard. When a normal user reads their timeline, the system checks: which of the users-they-follow are celebrities? For each celebrity, fetch their recent tweets (one Cassandra query each, batched), merge with the cached timeline, sort, return. Most users follow 1-5 celebrities, so this adds 1-5 extra queries — still fast (<100 ms).
Snowflake IDs. Tweet IDs must be sortable by time AND globally unique AND assignable without a coordinator. Twitter's Snowflake scheme packs a 41-bit millisecond timestamp + 10-bit worker_id + 12-bit sequence into a 64-bit integer. Each worker generates IDs independently; the timestamp gives sortability; the worker_id gives uniqueness across workers; the sequence gives uniqueness within a millisecond on a single worker.
Tweet ID encoding. Snowflake IDs are 64-bit ints, but URLs use base-62 encoded strings to keep them short (~11 chars).
Retweets. A retweet is a separate tweet row that references the original via retweet_of. Fan-out pushes the retweet_id, not the original. On timeline read, we hydrate retweets by fetching the original tweet (one batched Cassandra call).
Search indexing. Every tweet publish also goes to a Kafka topic tweets.v1. A consumer indexes each tweet into Elasticsearch for full-text search. The search path is decoupled from the write path — a search outage doesn't break tweeting.
Tombstones and delete. Deletes write a tombstone row in Cassandra. Timeline reads filter deleted tweet_ids at hydration. Old timeline entries are evicted by the ZSET cap (ZREMRANGEBYRANK).
Bottlenecks and failure modes.
-
Celebrity fan-out collapse. Without the celebrity bypass, Lady Gaga's tweet would write 80M Redis entries and saturate the timeline cluster. Mitigation: celebrity detection at >10K followers; pull-on-read for them.
-
Hot tweet. A viral tweet gets 1M likes/sec; the like-counter row is a write hotspot. Mitigation: shard the counter (e.g. 16 shards, sum on read) or use Cassandra counters (which are sharded by the DB itself).
-
Fan-out worker backlog. If Kafka or the worker pool falls behind, tweets take minutes to appear in timelines. Mitigation: autoscale workers on queue depth; alert if a tweet is >30s old before fan-out.
-
Redis timeline cluster failure. Without the cache, every timeline read becomes a fan-out-on-read across the user's followed accounts — Cassandra gets crushed. Mitigation: Redis cluster with replicas + circuit breaker that returns stale timelines or 503-with-retry.
-
Cassandra compaction spikes. Tweets generate huge write volume; compaction spikes cause p99 latency spikes. Mitigation: tiered compaction for tweets, leveled for the smaller tables; bound compaction throughput.
-
Search index lag. The Kafka -> ES pipeline has 5-30s lag; searching for a tweet you just posted may miss it. Mitigation: accept it (most search is for older content); show 'this tweet was just posted, indexing in progress'.
-
Snowflake clock skew. If a worker's clock jumps backward, Snowflake emits duplicate or out-of-order IDs. Mitigation: NTP with bounded slew; reject IDs with timestamps in the past.
-
Reply threading. Replies are stored as tweets with
reply_to. Fetching a conversation thread requires fetching all tweets withreply_to = X, which is a secondary index lookup. Mitigation: maintain a separaterepliesmaterialized view keyed by the root tweet.
Scaling strategy and trade-offs.
Sharding. Tweet storage is sharded by tweet_id (which is itself a Snowflake ID, so the shard hash is uniform). Follows are sharded both ways (followers-by-follower and followers-by-followee materialized views).
Timeline cache. Redis cluster sharded by user_id. Each user's timeline is a ZSET of the last 800 tweet_ids. The cap bounds memory (250M users x 800 x 8B = 1.6 TB) and forces pull-on-read for older content.
Multi-region. Timeline cache is per-region; the Cassandra tweet store is globally replicated. Writes go to a single primary region (the tweet itself is small; cross-region replication is cheap).
Search scale. Elasticsearch cluster sharded by tweet hash. For very large clusters (>1B docs), use ES's rollover indices (one index per day, alias points to latest) so old indices can be searched less aggressively.
Trade-offs made explicit.
- We chose hybrid fan-out — gained cheap celebrity tweets, lost uniform read latency (celebrity-following users pay extra query cost).
- We chose Cassandra for tweets — gained write throughput and time-series friendliness, lost JOINs (hydrating a timeline requires a multi-get, not a JOIN).
- We chose Snowflake IDs — gained no-coordinator uniqueness + sortability, lost any ordering guarantee across workers in the same millisecond.
- We chose Elasticsearch for search — gained flexible full-text search, lost a separate operational system to run (ES clusters are notoriously finicky).
- We chose async fan-out (return 200 before fan-out completes) — gained fast post latency for the user, lost immediate visibility to followers (a tweet may take 1-2s to appear in a follower's timeline).
Elon Musk tweets. With pure fan-out-on-write and 150M followers, how many Redis writes happen, and what's the problem?
Pick one answer.
Why use Snowflake IDs (timestamp + worker_id + sequence) instead of a global auto-increment counter?
Pick one answer.
Engineering mental model
Mental model. Think of Design Twitter as a deliberate boundary in a system. The boundary exists because something becomes harder to manage when everything is done in one place: latency, scale, failure isolation, consistency, cost, or team ownership. The useful question is not “what does Design Twitter mean?” but “what pressure makes this boundary worth introducing, and what new failure mode does it create?”
Before choosing Design Twitter, name the workload, the critical user path, the dominant bottleneck, the failure you are trying to absorb, and the trade-off you are willing to accept. If you cannot name those five things, the design is probably premature.
// Pseudocode
request = receive()
result = design_twitter(request)
return result
// Production questions:
// 1. What happens on timeout?
// 2. Can this operation be retried safely?
// 3. What is the bottleneck?Back-of-the-envelope reasoning
Numerical lens: write down traffic, payload size, read/write ratio, peak multiplier and durability target before choosing a component. The numbers should justify the architecture.
Interactive thought experiment: Design Twitter
Change the variables below and predict what breaks first in Design Twitter. The production lab can later reuse these same inputs.
Change one variable at a time. Predict the failure mode first, then move the slider and see whether your mental model matches the simplified system response.
If you are stuck on Design Twitter, start by drawing the request path and marking every network hop, stateful component, queue, cache and failure boundary. Then estimate where the system will saturate.
You increase traffic by 10× in a system using Design Twitter. What should you inspect first?
Pick one answer.
Which statement is the safest engineering habit when using Design Twitter?
Pick one answer.
You have dashboards for traffic, latency, errors and saturation. You can change the architecture, but every change has operational cost.
Production scenario: your system uses Design Twitter, traffic suddenly spikes, and p99 latency doubles. What is your first move?
Interview drill
Answer this without notes: When would you choose Design Twitter, and when would you intentionally avoid it? Mention at least one bottleneck it addresses, one failure mode it introduces, and one alternative. Then quantify the workload you are designing for.
A useful engineering lens for Design Twitter: define the problem it solves, the simpler design that fails first, the constraint that forces you to introduce this concept, and the new failure modes the concept creates.
Numerical sanity check
Back-of-the-envelope reasoning beats fake precision. State your traffic, payload, concurrency and growth assumptions explicitly, then calculate enough to know whether the current architecture is orders of magnitude away from the target.
Do not optimize for a memorized definition. Reason from the workload and failure mode.
Imagine the simplest version of a system using Design Twitter. What breaks first as traffic grows by 10×, and what would you change before reaching 100×?
Pick one answer.
What you gain, what you pay
- +Snowflake IDs give globally-unique, time-sortable tweet IDs with no coordinator.
- +Hybrid fan-out (push normal, pull celebrity) bounds per-tweet write amplification.
- +Redis ZSET timeline cache gives sub-100ms reads for the common case.
- +Async Kafka -> Elasticsearch pipeline decouples search from the write path.
- −Celebrity tweets still stress the pull path — needs celebrity post caching.
- −Cassandra compaction causes latency spikes — needs careful compaction strategy.
- −Snowflake clock skew causes duplicate / out-of-order IDs.
- −Timeline cache death cascades into DB collapse without a circuit breaker.
How this breaks in production
- Celebrity fan-out collapse without the >10K bypass.
- Like-counter hotspot on viral tweets — needs counter sharding.
- Fan-out worker backlog delays tweet visibility — needs autoscaling.
- Search index lag (5-30s) — recently posted tweets may be unsearchable briefly.
- Snowflake clock skew causing out-of-order IDs.
Don't fall into these traps
- •Pure fan-out-on-write for everyone — crushed by celebrity tweets.
- •Using a global auto-increment counter for tweet IDs — single point of failure.
- •Synchronous fan-out (blocking the tweet POST until fan-out completes) — slow posts.
- •Storing timeline as the full tweet text instead of just tweet_ids — wastes cache.
- •Single Elasticsearch cluster for everything — no sharding strategy.
Real systems using this
How real systems implement this
- Twitter / X — Hybrid fan-out with celebrity bypass at ~10K followers. Snowflake IDs. Cassandra for tweet store, Redis (Gemini) for timelines. Documented in their engineering blog 'Timeline scalability at Twitter'.
- Bluesky (AT Protocol) — Federated fan-out-on-write via PDS -> BGS -> AppView. Each user has a personal data server that fans out their writes to a relay that subscribers poll.
- Mastodon — ActivityPub-based federated fan-out: each instance pushes new posts to followers' instances via HTTP.
Practice saying it out loud
- Q1Design Twitter. How do you handle a tweet from a celebrity with 100M followers?
- Q2How do you generate unique, sortable tweet IDs without a coordinator?
- Q3Your timeline Redis cluster dies. What's the impact?
- Q4How do you make search not block the tweet write path?
Further reading & references
Core explanations are original NO CAP material. External references are provided for deeper study and standards.
What next?
Mark as understood once the mental model clicks.
Next recommended
Design Instagram