Guide contents

Lesson 123 of 129

Design Twitter

A microblogging service lets users publish short posts (tweets), follow other users, and read a home timeline: a reverse-chronological or ranked feed of posts from accounts they follow. The defining difficulty is the asymmetry between reads and writes and the extreme skew in follower counts. This article extends the general news feed design with details specific to a Twitter-like product.

Requirements

Functional

  • Post a tweet (text up to a fixed length, optional media).
  • Follow and unfollow users.
  • Home timeline: recent tweets from followed accounts.
  • User timeline: a user's own tweets.
  • Like, retweet, and reply.
  • Search tweets and show trending topics.

Non-functional

  • Timeline reads are fast (p99 under a few hundred ms).
  • High availability; eventual consistency is acceptable (a tweet may appear in followers' timelines seconds later).
  • Highly read-heavy.
  • Handle celebrity accounts with tens of millions of followers.
  • Durable tweet storage.

Estimation

Assumptions: 300 million daily active users (DAU), each reads timelines 10 times a day and 20% of users tweet, averaging 2 tweets per posting user per day. Average 200 followers.

  • Tweets: 300 x 10^6 x 0.2 x 2 = 120 x 10^6 tweets/day ≈ 1,400 writes/s average.
  • Timeline reads: 300 x 10^6 x 10 = 3 x 10^9/day ≈ 35,000 reads/s average, roughly 100,000 at peak.
  • Read to write ratio is about 25:1.
  • Fan-out on write: 1,400 tweets/s x 200 followers = 280,000 timeline inserts/s.
  • Tweet storage: 120 x 10^6 x ~300 bytes (text plus metadata) = 36 GB/day, about 13 TB/year before replication. Media is stored separately in object storage and served via a CDN.
  • Timeline cache: 300 x 10^6 users x 800 tweet IDs x 8 bytes = 1.9 TB. Only IDs are cached, so this fits in a Redis cluster.
  • Read bandwidth: 100,000 timeline reads/s at peak × 20 tweets × ~300 bytes ≈ 600 MB/s of tweet payload (about 5 Gbit/s) before media; tweet objects are read far more often than written, so a tweet cache hit ratio above 95 percent matters more than the database.
  • Fan-out skew: with an average of 200 followers, 280,000 inserts/s is manageable, but one account with 50 million followers is 50 million inserts for one tweet. At 100,000 inserts/s per worker fleet slice that alone would take minutes.

Implications: 1,400 writes/s of tweets is small, so the tweet store is easy; the hard parts are 280,000 timeline inserts/s (needs a queue and many workers, not synchronous fan-out in the post request), the 1.9 TB timeline cache (sharded memory, and bounded to the recent 800 entries), and the celebrity tail (needs the hybrid model). A follower threshold of roughly 10,000 to 100,000 bounds the work per tweet; below it, push.

API design

Code
POST /v1/tweets                {text, media_ids, reply_to?}      -> {tweet_id}
DELETE /v1/tweets/{id}
POST /v1/users/{id}/follow
DELETE /v1/users/{id}/follow
GET  /v1/timeline/home?cursor=<tweet_id>&limit=20
GET  /v1/users/{id}/tweets?cursor=&limit=
POST /v1/tweets/{id}/like
GET  /v1/search?q=&cursor=
GET  /v1/trends?region=

Timelines use cursor pagination on tweet ID rather than offset, because new tweets arrive continuously and would shift offsets. Tweet IDs are time-sortable (Snowflake-style IDs), so sorting by ID sorts by time.

Data model

Table Key Fields Store
users user_id handle, profile, counts Sharded relational or key-value
tweets tweet_id author_id, text, media, created_at, reply_to Sharded by tweet_id in a key-value or wide-column store
user_tweets (author_id, tweet_id desc) tweet_id Wide-column, partitioned by author
follows (follower_id, followee_id) and reverse timestamp Two tables: "who I follow" and "who follows me", partitioned by first key
home_timeline user_id list of tweet_ids (bounded) Redis lists or sorted sets
likes (tweet_id, user_id) timestamp Wide-column; counters aggregated asynchronously

Reasoning: the access patterns are known and simple (lookup by key, range by author), which suits key-value and wide-column stores partitioned by ID (key-value stores, sharding). Relational joins across users and tweets do not scale here, so the timeline is precomputed rather than joined at read time. The follow graph is stored in both directions because both queries ("who does X follow" for pull, "who follows X" for fan-out) are hot.

High-level design

flowchart LR
    Client --> GW[API gateway]
    GW --> TS[Tweet service]
    GW --> TL[Timeline service]
    GW --> SG[Social graph service]
    GW --> SS[Search service]
    TS --> TDB[(Tweet store)]
    TS --> Q[[Message queue]]
    Q --> FO[Fan-out workers]
    FO --> SG
    FO --> TC[(Timeline cache)]
    Q --> IDX[Search indexer]
    IDX --> ES[(Search index)]
    Q --> TR[Trends stream job]
    TL --> TC
    TL --> TDB
    TL --> SG

Components

  • Tweet service validates and stores tweets, then publishes a tweet_created event to a queue (Kafka).
  • Fan-out workers consume the event, look up the author's followers, and insert the tweet ID into each follower's cached timeline.
  • Timeline service reads the precomputed timeline from cache, merges in tweets from celebrity accounts, hydrates tweet IDs into full tweets (batch fetch), and returns them.
  • Social graph service stores follow edges and serves follower and followee lists.
  • Search indexer and trends job consume the same event stream.
sequenceDiagram
    participant A as Author client
    participant T as Tweet service
    participant Q as Queue
    participant F as Fan-out worker
    participant C as Timeline cache
    participant R as Reader client
    participant L as Timeline service
    A->>T: POST /tweets
    T->>T: assign ID, persist
    T-->>A: 201 tweet_id
    T->>Q: tweet_created
    Q->>F: event
    F->>F: get follower list
    F->>C: push tweet_id to each follower list
    R->>L: GET /timeline/home
    L->>C: read tweet_id list
    L->>L: hydrate tweets, merge celebrity tweets
    L-->>R: timeline page

Deep dive

Fan-out strategy

There are two basic ways to build a timeline.

Approach Write cost Read cost Weakness
Fan-out on write (push): at post time, insert the tweet ID into every follower's timeline cache O(followers) per tweet O(1) cache read A tweet by an account with 50 million followers causes 50 million inserts; wasted work for inactive followers
Fan-out on read (pull): at read time, fetch recent tweets from each followed account and merge O(1) O(followees) per read Slow reads when a user follows many accounts; read-heavy workload multiplies cost

A hybrid approach is used in practice. Push for most authors; for accounts above a follower threshold, skip fan-out and mark them as "celebrity". At read time the timeline service takes the precomputed list, then pulls recent tweets from the celebrity accounts that the reader follows, and merges by tweet ID. Additional refinements: push only to followers who were recently active, and rebuild a cold user's timeline on demand at their next login.

Why hybrid, and with what numbers. Pure push makes posting cost unbounded and wastes inserts for inactive users (a large share of followers may not log in for days). Pure pull makes each read touch every followee: a user following 500 accounts needs 500 lookups per read, at 100,000 reads/s that is 50 million lookups/s. Hybrid keeps reads at one cache read plus a merge of a few celebrity lists. Concretely: push when the author has fewer than about 10,000 followers (tunable); above it, store the tweet only in the author's user_tweets partition. A reader follows perhaps 5 to 20 celebrities on average, so the read-time pull is 5 to 20 cached lookups. Fan-out workers consume from Kafka partitioned by author ID for per-author ordering, with at-least-once delivery. Inserting the same tweet ID into a sorted set twice is idempotent, so duplicates are harmless. Split a large follower list into batches of, say, 1,000 IDs per queue message so a mid-size author's fan-out is parallel and a crashed worker retries only its batch. Active-follower fan-out (those seen in the last 30 days) can cut inserts by roughly half or more.

Sharding the timeline cache. Shard by user_id (hash), so one read touches one shard and one insert touches one shard. Sharding by author would make readers gather from many shards. A hot shard occurs only if many heavy readers hash together, which is rare; a celebrity's own list is read by many users, so replicate or locally cache celebrity user_tweets (TTL of a few seconds).

Timeline storage and hydration

Each timeline is a bounded list (for example, the latest several hundred IDs) held in an in-memory store. Storing IDs rather than tweet bodies keeps memory small and avoids updating many copies when a tweet is deleted or edited. Hydration issues a batched multi-get to the tweet store and to caches for counts and author profiles. Deleted tweets are filtered at hydration time, which avoids scanning every follower's list on delete.

Numbers and choices: cap each list at about 800 IDs (older pages fall back to a pull from user_tweets); use a Redis sorted set or list, which costs a few tens of bytes per entry, so real memory per timeline is a few times the 8-byte raw ID, and the 1.9 TB above is a lower bound (plan for 2 to 3x). Timeline cache entries are replicated for availability (one replica), and losing them is safe because they can be rebuilt. Tweet objects use a look-aside cache with a TTL of hours and explicit invalidation on delete or edit. The tweet store is eventually consistent (replicas within tens of milliseconds); read-your-writes is handled by merging the author's own recent tweets client-side or server-side. The alternative to a cached list, computing the timeline from stored edges on demand, is what pull does; it needs no cache but costs O(followees) per read.

Likes, retweets, and counters

A like is a row keyed by (tweet_id, user_id). The composite key makes the write idempotent, so a repeated request does not count twice, and it answers "did this user like this tweet" with one key lookup. The displayed like count is a separate counter. Incrementing one counter row per like makes a viral tweet a write hot spot, so increments are buffered in memory or a queue and applied in batches, or spread across several counter shards per tweet that are summed at read time. The count shown to readers can lag the true value by seconds.

A retweet is a new tweet record that references the original tweet ID. It is fanned out like any other tweet, and hydration replaces the reference with the original content. A reply stores reply_to and appears in the conversation view built from a query by reply_to, not in followers' home timelines by default.

Trends require counting hashtags and phrases in a sliding window and detecting unusual growth. A stream processor consumes tweet_created events, extracts hashtags, and maintains windowed counts per region, for example with a count-min sketch or exact counters in a keyed stream job (see probabilistic data structures and stream vs batch processing). A topic trends when its current window count is high relative to its historical baseline, not merely when the raw count is high. Results are written to a small store refreshed every few minutes and cached.

Tweets are indexed by a search cluster built on an inverted index. Indexing is asynchronous from the event stream, so new tweets appear in search after a short delay. Recent tweets are kept in a memory-resident index tier, older ones in disk-based tiers. Queries fan out over time-partitioned shards and merge by relevance or recency.

Bottlenecks and scaling

  • Celebrity fan-out. Mitigated by the hybrid model and per-author thresholds.
  • Hot tweets. Viral tweets create hot keys for reads and like counters. Cache tweet objects with replication of hot keys, and buffer counter updates, aggregating them asynchronously. See hot key mitigation.
  • Fan-out lag. Monitor queue lag; scale workers by partitioning on follower list shards, not only by author.
  • Timeline cache loss. Timelines are derived data. Rebuild from the tweet store and follow graph on cache miss.
  • Geography. Serve reads from regional replicas; media through CDN.

Failure modes

What breaks How it degrades Mitigation
Fan-out queue lags (backlog) Followers see tweets late; posting still succeeds because the tweet is persisted first Alert on consumer lag; add workers and partitions; prioritize small authors and active users; let readers pull recent tweets for the accounts they follow as a fallback
Timeline cache shard lost Those users' timelines rebuild on demand (slow first load, higher tweet-store and graph load) Replicas; rebuild rate limit; serve a partial timeline from a pull of the top followees while rebuilding
Tweet store shard slow or down Hydration is missing tweets for that shard Return the timeline with the missing entries omitted; read from replicas; cache tweet objects
Social graph service down New follows fail and fan-out stalls Fan-out retries from the queue; timelines still serve from the cache
Search or trends down Feature unavailable; core timeline unaffected Isolate as separate consumers of the event stream; replay from Kafka to catch up
Viral tweet Hot key on tweet object, counters, and reply lists Replicated hot-key cache, sharded counters, rate-limited reply fetches

Order of importance: posting and reading the home timeline must survive the failure of search, trends, and counts, which degrade to stale or absent. At 10x (3 billion DAU-equivalent traffic, 14,000 tweets/s, 1,000,000 timeline reads/s): fan-out inserts reach 2.8 million/s if the follower average is unchanged, which pushes the celebrity threshold lower, adds a filter for inactive users, and turns the timeline cache into tens of terabytes (hundreds of shards). Multi-region becomes mandatory, with per-region timeline caches fed by cross-region replication of the tweet event stream.

Trade-offs and follow-up questions

  • Consistency. A follower may not see a new tweet for seconds. The author should see their own tweet immediately (read-your-writes), typically by merging their own recent tweets into their timeline at read time.
  • Ranked vs chronological. Ranking adds a scoring stage and feature storage after candidate retrieval; chronological is a simple merge by ID.
  • What if a normal user becomes a celebrity (or the reverse)? Recheck the follower count on follow events; when crossing the threshold, stop fanning out new tweets. Old pushed entries remain in timelines and are deduplicated at merge by tweet ID.
  • What if the celebrity threshold is set too low? More authors are pulled at read time and read latency grows with the number of celebrities followed; too high, and fan-out lag grows. Tune it from measured queue lag and read fan-in.
  • What if a celebrity tweets and 10 million users refresh at once? The pull path hits one user_tweets partition; serve it from a replicated cache with a short TTL, and coalesce concurrent misses.
  • What if we need ranked timelines? Retrieve about 500 candidates from the same push and pull sources, then score with a model; keep the chronological path as a fallback if the ranker is down.
  • What if two people like at the same instant? The composite key makes each like idempotent; counters are eventually consistent, so the count may briefly be off by a few.
  • How do you delete a tweet quickly across all systems? Mark it deleted in the tweet store first (hydration filters it immediately), then publish a delete event to remove it from search and caches.
  • Follow-ups: How are unfollows applied to cached timelines? (Filter at read time against the current follow set, and lazily remove.) How would you support edit history? How would you handle a user with 100 million followers deleting a tweet? How would you add notifications (notification system)?