How Twitter Served 300,000 Timelines Per Second
_This post is a plain breakdown of how Twitter handled timeline scale, why the first approach broke, and why the final solution is a hybrid._
The Problem
When I strip Twitter down to the basics, there are really only two product operations that matter:
- Post Tweet: a user publishes a new message to their followers
- Home Timeline: a user views tweets from the people they follow
Here are the numbers Twitter published (Nov 2012):
Scroll horizontally to view all columns
| Operation | Average | Peak |
|---|---|---|
| Post Tweet (writes) | 4,600 req/sec | 12,000 req/sec |
| Home Timeline (reads) | 300,000 req/sec | — |
The read-to-write ratio is roughly 65x. People read way more than they write. That asymmetry is the whole story here.
What surprised me the first time I studied this: 12,000 writes/sec is not the scary part. A solid relational setup can handle that. The hard part is _fan-out_: one tweet may need to show up in millions of home timelines almost immediately.
The Schema
At the core, Twitter's model has three tables.
tweets: the content
Scroll horizontally to view all columns
| id | sender_id | text | timestamp |
|---|---|---|---|
| 1 | 12 | "Acquitted again, very legal" | 1000 |
| 2 | 5 | "Opposition found alive (mistake)" | 1001 |
| 3 | 12 | "Bankruptcy is just a strategy" | 1002 |
| 4 | 8 | "Missile landed on... somewhere. Geography is hard." | 1003 |
Every tweet lives here. Notice sender_id = 12 appears twice: rows 1 and 3 are both from Trump. This table is just tweet content, nothing else (and totally not legally incriminating).
users: the profiles
Scroll horizontally to view all columns
| id | screen_name | profile_image |
|---|---|---|
| 5 | putin | putin.jpg |
| 8 | kim_jong_un | kim_jong_un.jpg |
| 12 | trump | trump.jpg |
This is profile data only. No tweets, no follow graph. If I see sender_id = 12 in tweets, I resolve id = 12 here and get "trump".
follows: the relationships
Scroll horizontally to view all columns
| follower_id | followee_id | meaning |
|---|---|---|
| 100 | 5 | User 100 follows Putin |
| 100 | 12 | User 100 follows Trump |
| 101 | 12 | User 101 follows Trump |
This table stores the follow graph. One row = one relationship.
Follower vs. Followee
I used to mix this up, so here is the simple version:
- Follower: the person doing the following. If you follow Trump, _you_ are the follower.
- Followee: the person being followed. Trump, in this case, is the followee.
In follows, follower_id = 100 means user 100 is doing the following. followee_id = 12 means user 12 is being followed.
So when Trump tweets, the question is: _"Who has followee_id = 12?"_ That result is everyone whose timeline might need an update.
Approach 1: Query at Read Time
Twitter's original approach was straightforward: writes go into tweets, and timelines are computed on demand at read time.
The SQL for fetching a home timeline:
SELECT tweets.*, users.*
FROM tweets
JOIN users ON tweets.sender_id = users.id
JOIN follows ON follows.followee_id = users.id
WHERE follows.follower_id = current_user
ORDER BY tweets.timestamp DESCWhat each line does
SELECT tweets., users.: grab tweet content plus author info.
FROM tweets: start from tweets.
JOIN users ON tweets.sender_id = users.id: attach author profile to each tweet.
JOIN follows ON follows.followee_id = users.id: keep tweets from accounts the viewer follows.
WHERE follows.follower_id = current_user: filter everything down to one user's home timeline.
Tracing through the example
Say I am user 100 and I follow Putin (5) and Trump (12):
- The query looks in follows where follower_id = 100: finds followee_id 5 and 12
- Matches those followee IDs against users: retrieves Putin and Trump's profiles
- Matches those user IDs against tweets via sender_id: retrieves tweets 1, 2, and 3
- Kim Jong Un's tweet (id = 4, sender_id = 8) never appears: user 8 is not in your follows
Why this broke at scale: 300,000 timeline reads/sec means hammering this multi-join query constantly. Even with indexes, this gets expensive fast. That pushed Twitter to move work away from reads.
Approach 2: Fan-Out on Write
The key idea I took from this: if reads outnumber writes by 65x, pay more at write time so reads are cheap.
Instead of building timelines on demand, build them when tweets are created. Each user gets a precomputed cached timeline (like a mailbox), ready to read.
The mailbox analogy
This is what happens when Trump tweets:
- The tweet is saved to the global tweets table
- A background worker queries: SELECT follower_id FROM follows WHERE followee_id = trump_id
- The worker gets back a list of all Trump's followers (say, 31 million people)
- For each follower, it prepends the new tweet to the front of their cached timeline
When I open the app, the timeline is basically a cache fetch. No heavy joins in the hot read path.
How the cache gets updated
The important part: this is incremental. We do not rebuild full timelines. We prepend the new tweet, which is O(1) on structures like Redis lists.
Before Jack's new tweet:
Your mailbox: [T3, T1]After Trump posts tweet T5 and the fan-out worker runs:
Your mailbox: [T5, T3, T1]The existing entries are untouched. Only the new tweet is inserted at the front.
The math
Average Twitter user has approximately 75 followers.
4,600 tweets/sec × 75 followers = 345,000 cache writes/secAt first glance, this looks like more total work. It is. But the work is cheaper:
- A cache write is a list prepend in Redis: microseconds, no disk I/O, no locking
- A JOIN query involves scanning indexed B-trees, merging results from multiple tables, sorting: milliseconds, disk-bound
So we trade 300,000 expensive read queries for roughly 345,000 cheap cache writes. In practice, that is often a huge win.
The Full Data Pipeline
Twitter tweet delivery pipeline diagram
The three numbers that matter:
Scroll horizontally to view all columns
| Stage | Rate | Nature |
|---|---|---|
| Raw tweet ingestion | 4,600 writes/sec | Cheap: one row insert |
| Fan-out to caches | 345,000 writes/sec | Fast: Redis list prepend |
| Timeline reads | 300,000 reads/sec | Trivial: cache fetch |
Approach 1 vs Approach 2
Scroll horizontally to view all columns
| Aspect | Approach 1 | Approach 2 |
|---|---|---|
| Work timing | Work done at read time | Work done at write time |
| Read cost | High: requires 3-table JOIN | Near-zero: cache fetch |
| Write cost | Low: single row insert | High: fan-out writes to many caches |
| Scaling issue | Breaks with high read volume | Breaks with users having massive follower counts |
| Twitter usage | Initially used | Later switched to this approach |
The Celebrity Problem — and the Hybrid Solution
Fan-out on write has a ceiling. The average account might have ~75 followers, but celebrity accounts have tens of millions. And autocrats with state media have _unlimited_ angry followers, whether they follow voluntarily or face consequences.
If one celebrity tweet triggers 30-80 million fan-out writes, queues back up and latency spikes. (See also: what happens when the guy with the nuclear button tweets at 3am.)
So the production answer is a hybrid:
- Normal users: fan-out on write (Approach 2). Their tweets are pre-distributed into follower caches immediately.
- Celebrities: no fan-out. Their tweets stay in the global tweets table.
When I request my home timeline, the system merges:
- Your pre-built cache (tweets from normal users you follow)
- A small real-time query for tweets from celebrities you follow (Approach 1)
This stays manageable because most users follow only a small number of celebrity accounts.
That is the big lesson for me: architecture decisions should follow workload shape, not ideology.
The Core Principle
This case study captures a tradeoff I keep seeing in distributed systems:
Do more work at write time so the common path is trivially cheap.
Twitter optimized for reads because reads dominated writes.
When I design systems now, I start with one question: _what is my read/write ratio, and where should I pay cost?_ That one answer often determines the rest of the architecture.
Related Posts
How does the internet work
A summary/overview of how the internet works in the simplest terms i could possibly use
The Art of Minimal Design
Exploring the principles of minimal design, how to achieve clean interfaces, and the balance between functionality and aesthetics in modern web applications.
Building Type-Safe APIs with TypeScript
Leveraging TypeScript's type system to create robust, maintainable APIs with end-to-end type safety.