Ask a candidate to design Twitter and most of them start in the wrong place. They draw a tweets table, a follows table, and a query that joins the two, ordered by time. It looks correct, it would even pass a code review for a small app, and it is completely unworkable at the scale Twitter runs. The moment you have a user who follows two thousand accounts, and three hundred thousand people asking for their home timeline every second, that innocent join becomes the most expensive query in the system, run at a rate no relational database will survive.
The interesting part of Twitter is not storing a tweet. A tweet is a short string with an author and a timestamp, and any database can hold billions of them. The interesting part is assembling the home timeline: the reverse-chronological, and later ranked, blend of tweets from everyone you follow, delivered in tens of milliseconds, for hundreds of millions of people at once. That assembly is the whole problem, and the technique at the center of it is called fanout. This piece works the timeline the way a staff engineer would work it in a design review. We start from what the timeline actually is and the arithmetic of its scale, then build the architecture, then spend most of our time on the genuinely hard part, the fanout tradeoff between doing work when a tweet is written and doing work when a timeline is read, and why the real answer is neither one nor the other. We finish with the data model, the two hot paths, a failure mode that only shows up at scale, and the ranking layer that sits on top. One note before we start. Every latency, throughput, and volume figure here is an industry-typical estimate chosen to drive the sizing, not a measured number from Twitter's production systems.
Two timelines, and only one of them is hard
Twitter shows you two different feeds and it is worth separating them, because they have opposite cost profiles. The user timeline is the list of tweets a single account has posted, shown on that account's profile. The home timeline is the merged feed of everyone you follow, shown when you open the app. These sound similar and they are not.

The user timeline is a single-author lookup. The home timeline is a merge across everyone you follow, and that merge is the entire engineering problem.
The user timeline is easy. To render a profile you fetch one author's recent tweets, a bounded query against a single partition keyed by that author. Even a celebrity with a hundred million followers has a cheap user timeline, because it depends only on how much that one account has posted, not on how many people are watching. The home timeline is the hard one. It depends on the union of everyone you follow, each of whom is posting on their own schedule, and it has to be current, ordered, and fast. The functional requirements are small: post a tweet, follow and unfollow accounts, and read a home timeline. The non-functional requirements are where the design lives. The home timeline read has to return in tens of milliseconds at the high percentiles, because it is the first screen a user sees and every hundred milliseconds of delay costs engagement. Timelines must be highly available, since a Twitter that cannot show you your feed is simply down. And the freshness expectation is soft but real: a tweet should appear in your followers' timelines within seconds, not minutes, for a normal account. Write those constraints down first and the architecture reads as a set of answers to them.
Do the arithmetic before the architecture
The numbers decide the design here more than in almost any other system, because the read and write volumes are enormous and lopsided in a specific way. Assume the service handles around 500 million tweets per day. That averages to roughly 6,000 tweets per second, and real traffic is spiky, so peaks during a major live event run ten to fifteen times higher, into the neighborhood of 100,000 tweets per second.

Tweets are written at thousands per second, but each write can multiply into thousands of timeline updates, and timelines are read at hundreds of thousands per second.
Now look at the reads. Home timeline requests come in at around 300,000 per second, roughly fifty times the tweet rate, because people read far more than they post. If you served each of those reads with the naive join across everyone the user follows, you would be issuing hundreds of thousands of multi-way merges per second, each touching hundreds or thousands of accounts. That is the query that does not survive. The write side has its own multiplier. A single tweet does not cause a single write. It causes one write to store the tweet, plus one write into the home timeline of every follower who needs to see it. If an author has 500 followers, one tweet becomes 501 writes. Across the whole service this write amplification is the dominant load: on the order of a million timeline deliveries per second, tens of billions per day, dwarfing the 500 million tweets that triggered them. Storage for the tweets themselves is modest, a few hundred bytes each, a few hundred gigabytes a day, easily partitioned. The expensive resource is not disk. It is the fanout work and the memory to hold precomputed timelines, and that is where the engineering budget goes.
The high-level architecture
With the constraints and the arithmetic in hand, the components fall out. A client hits an API tier through a load balancer. The API tier is stateless and routes to the services that do the real work, and there are only a handful that matter for the timeline.

A write flows through the tweet store and the fanout service into per-user timeline caches. A read pulls a precomputed timeline and hydrates it. The social graph feeds both.
The tweet store is the durable source of truth for tweet content, keyed by tweet ID. The social graph service stores who follows whom and, critically, can answer both directions of that question: give me everyone this user follows, and give me everyone who follows this user. The fanout service runs on the write path. When a tweet is posted, it looks up the author's followers and pushes the tweet ID into each of their home timelines. The timeline cache, in practice a large fleet of Redis instances, holds each user's home timeline as a precomputed list of tweet IDs. The timeline service runs on the read path: it fetches the precomputed list, merges in anything that was not fanned out, and hydrates the IDs into full tweets before returning them. The organizing idea, and the thing that makes the whole system work, is that the home timeline is not computed when you read it. It is computed when the tweets that belong in it are written, and stored ready to serve. Reading becomes cheap because writing did the work in advance. That single inversion is the answer to the 300,000 reads per second, and it is also the source of every hard problem that follows.
The fanout problem
Here is the move stated plainly, because everything else is a consequence of it. Instead of computing your timeline on demand by querying everyone you follow, the system precomputes it. Every time someone you follow posts, their tweet is pushed into a list that is already sitting in memory with your name on it. When you open the app, your timeline is just there, a list of IDs ready to hydrate. This is fanout-on-write, also called push, and it is why Twitter feels instant.

One tweet from an account with many followers becomes one write per follower. This write amplification is the price of a precomputed timeline, and it is not evenly distributed.
The timeline is not a query you run. It is an answer you precompute, and the entire design is a negotiation over who pays for that precomputation and when.
The catch is written into the picture. Precomputing every timeline means that a single tweet triggers as many writes as the author has followers. For an account with a few hundred followers this is fine, a few hundred cheap writes into Redis lists, done in well under a second. But followers are not distributed evenly. They follow a power law so extreme that it breaks the model at the top end. A typical account has a few hundred followers. A large account has millions. The largest accounts have tens or hundreds of millions. When one of those accounts tweets, fanout-on-write asks the system to perform tens of millions of writes for a single post, and to do it fast enough that the tweet is not stale by the time it lands. That is the fanout problem: the same technique that makes reads instant makes writes for high-follower accounts catastrophically expensive. The rest of the design is about resolving that tension, so before we resolve it, we should look honestly at the two pure strategies and where each one falls apart.
Push versus pull
There are two pure ways to build a timeline, and they are mirror images. Fanout-on-write, push, does the work when a tweet is created. Fanout-on-read, pull, does the work when a timeline is requested.

Push pays at write time and makes reads trivial. Pull pays at read time and makes writes trivial. Each optimizes one side by punishing the other.
Push precomputes. When you tweet, the system writes your tweet into every follower's stored timeline immediately. Reads are then almost free, a single fetch of a ready-made list, which is exactly what you want when reads outnumber writes by an order of magnitude. The cost is write amplification, paid in full by high-follower accounts, plus the memory to store a materialized timeline for every active user. Pull does the opposite. When you tweet, nothing else happens; the tweet just sits in your user timeline. When someone requests their home timeline, the system queries the recent tweets of everyone they follow, merges those streams in time order, and returns the result. Writes are trivial, one insert, and there is no amplification no matter how many followers you have. The cost lands on reads: every home timeline request becomes a scatter-gather across hundreds or thousands of accounts, computed from scratch, at 300,000 requests per second. Push makes the common operation, reading, cheap by making the rarer operation, writing, expensive, which is the right direction for Twitter's ratio. Pull keeps writes cheap but puts the crippling cost on the operation that happens most. Neither pure strategy is acceptable on its own, and the reason is the same power law that created the fanout problem in the first place.
Where each model breaks
The two strategies do not fail randomly. They fail on specific, identifiable users, and once you see which users break which model, the hybrid answer designs itself.

Push breaks on authors with huge follower counts. Pull breaks on readers who follow huge numbers of active accounts. The failures sit at opposite ends of the graph.
Push breaks on the author side, on accounts with enormous follower counts. A celebrity tweet under pure push means tens of millions of writes, which saturates the fanout fleet, delays delivery so the tweet reaches different followers seconds or minutes apart, and wastes work writing into timelines that many followers will never open. Pull breaks on the reader side, on accounts that follow a very large number of active people. A user following five thousand busy accounts forces a five-thousand-way merge on every timeline load, and that cost is paid on the hot read path at full read volume. Notice that these failure modes are disjoint. The users who blow up push, high-follower authors, are a tiny, identifiable set. The users who blow up pull, high-following active readers, are also a minority. The overwhelming majority of interactions, a normal-follower author read by a normal-following user, are handled beautifully by push. So the design does not need to pick a strategy for the whole system. It needs to pick a strategy per relationship, using push where push is cheap and pull where push is ruinous. That is the hybrid model, and it is the actual answer to how Twitter's timeline works.
The hybrid answer
The production design is a hybrid: fanout-on-write for the vast majority of accounts, and fanout-on-read for a small set of very high-follower accounts, with the two merged at read time. This is the single most important idea in the whole system.

Normal accounts are pushed into follower timelines at write time. Celebrity tweets are pulled at read time and merged in. The threshold is a follower count, not a rule of thumb.
When a normal account tweets, the system fans out on write as before, pushing the tweet ID into the stored timeline of every active follower. When a designated high-follower account tweets, the system does not fan out at all. It simply stores the tweet and does nothing to followers' timelines, because writing to tens of millions of them is exactly the work we are trying to avoid. The merge happens on read. When you open your home timeline, the timeline service fetches your precomputed list, which already contains everything from the normal accounts you follow, and then separately fetches the recent tweets of the small number of celebrity accounts you follow, pulling those live from their user timelines. It merges the two streams in time order and returns the blend. The elegance is that each side does what it is good at. The many normal accounts are handled by cheap push, so your timeline is mostly precomputed. The few celebrity accounts, whose tweets would cost tens of millions of writes to push, are handled by pull, and pulling them is cheap precisely because there are so few of them per reader, usually a handful. The threshold for treating an account as pull, rather than push, is an operational tuning decision based on follower count, and it can be adjusted as the fanout fleet's capacity changes. This is the design that lets one architecture serve both the account with two hundred followers and the account with ninety million.
Snowflake IDs and why order is not an afterthought
A timeline is fundamentally an ordered merge, and ordering at this scale needs identifiers that sort correctly on their own, without a coordinating lookup. Twitter solved this with Snowflake, a scheme for generating 64-bit IDs that are unique across a distributed fleet and, importantly, roughly time-ordered.

A Snowflake ID packs a millisecond timestamp, a machine identifier, and a per-millisecond sequence into 64 bits, so sorting by ID is sorting by time without any central counter.
The layout is a bit field inside a 64-bit integer. The leading bit is left unused so the value stays positive as a signed integer. The next 41 bits hold a millisecond timestamp measured from a custom epoch, which is enough to represent about 69 years. The next 10 bits identify the machine or worker that generated the ID, allowing 1,024 generators to run in parallel. The final 12 bits are a per-machine sequence number that increments for multiple IDs created within the same millisecond, allowing 4,096 IDs per machine per millisecond. There is no central counter and no coordination between generators, so ID assignment never becomes a bottleneck, which matters when you are minting IDs at tens of thousands per second. The payoff for the timeline is direct. Because the timestamp occupies the high bits, sorting tweet IDs numerically sorts them chronologically. A home timeline stored as a list of tweet IDs is therefore already in time order, and merging a pulled celebrity stream into a pushed timeline is a merge of two sorted lists of integers, which is about as cheap as merging gets. Ordering is not something the timeline computes at read time from timestamps in the tweet bodies. It is baked into the identifier, which is why the identifier scheme is part of the timeline design and not a detail left to the storage layer.
The data model and where each piece lives
The storage choices follow directly from the access patterns, and there are three distinct patterns, so there are three distinct stores. Trying to serve all of them from one database is the mistake the naive design makes.

Three access patterns, three stores: durable tweets keyed by ID, a bidirectional follow graph, and per-user timelines held as lists of IDs in memory.
The tweet store holds tweet content, keyed by the Snowflake tweet ID, and it is the durable source of truth. The access pattern is get-by-ID and get-recent-by-author, which is a partitioned key-value or wide-column workload, not a relational one. The social graph store holds follow edges and must answer both directions efficiently, followers-of and following-of, because fanout needs the follower list and the pull path needs the following list. This is a specialized graph or adjacency store tuned for fast, large adjacency reads. The timeline cache is the interesting one. Each user's home timeline is stored in Redis as a list of tweet IDs, not full tweets, capped at several hundred entries, commonly around 800, because nobody scrolls back further than that in practice and an unbounded list would waste memory. Storing IDs rather than tweet bodies is a deliberate normalization: the tweet content exists once in the tweet store, and the timeline holds only pointers to it, so a tweet edited or deleted at the source does not have to be chased across millions of timelines. The tradeoff is that reading a timeline requires a second step, hydration, to turn the list of IDs into displayable tweets, which we handle on the read path. Three access patterns, three purpose-built stores, and the timeline store deliberately holds references rather than data.
The write path
Posting a tweet is a short synchronous step followed by an asynchronous fanout, and the split between the two is what keeps the user's post feeling instant even though it triggers thousands of downstream writes.

The synchronous part is small: persist the tweet and return. Fanout runs asynchronously, pushing the tweet ID into follower timelines and skipping celebrity accounts entirely.
When a user posts, the tweet service first assigns a Snowflake ID and writes the tweet durably to the tweet store. As soon as that write is confirmed, the user gets a success response; their post is safely stored and visible on their own profile. Everything after that happens asynchronously, off the request the user is waiting on. The tweet is handed to the fanout service, usually by way of a durable queue so that a fanout worker crash cannot lose deliveries. The fanout service checks whether the author is a push account or a pull, celebrity, account. If the author is a celebrity, fanout stops here; the tweet stays in the tweet store to be pulled at read time, and no follower timelines are touched. If the author is a normal account, the fanout service asks the social graph for the author's followers, filters to those with a materialized timeline, meaning active users, and pushes the tweet ID onto the front of each of their Redis timeline lists, trimming each list back to its cap. Filtering to active users is a real optimization, not a nicety: there is no reason to maintain a live timeline for an account that has not logged in for months, so dormant users' timelines are dropped and rebuilt on demand when they return. The write the user waited for was one durable insert. The thousands of timeline writes happen behind the queue, where their latency and their spikes cannot hurt the person who tweeted.
The read path
Reading the home timeline is the operation that runs 300,000 times a second, so it has to be close to a single fetch, and the hybrid model plus the ID-list storage make that possible.

Fetch the precomputed list, pull the handful of celebrity accounts live, merge the two sorted streams, hydrate the IDs into tweets, then filter. Only the last steps touch the tweet store.
The timeline service starts by fetching the user's precomputed timeline from Redis, a single read that returns a list of tweet IDs already in time order. It then handles the pull side of the hybrid: it looks up which celebrity accounts this user follows, a small list, and fetches their recent tweet IDs live from their user timelines. Merging the pushed list and the pulled celebrity tweets is a merge of sorted integer lists, cheap because Snowflake IDs sort by time. Now the service has an ordered list of tweet IDs and needs to turn them into displayable tweets, the hydration step: it batch-fetches the tweet content for those IDs, almost always from a tweet cache in front of the tweet store, since the same popular tweets are hydrated across many timelines and cache extremely well. Finally it applies read-time filtering: dropping tweets from accounts the user has since blocked or muted, removing tweets that were deleted at the source after being fanned out, and enforcing visibility rules. That last point is why holding IDs rather than tweet bodies pays off, because a deleted tweet simply fails to hydrate or is filtered, and the system never had to reach into millions of timelines to scrub it. The read touched Redis once for the list, made a small live pull, and hit a well-cached tweet store for hydration. No joins, no scatter-gather across the follow graph, nothing that scales with how many people you follow on the hot path.
The timeline in Redis, and what it costs
The precomputed timelines live in memory because the read path cannot afford disk, and understanding their memory footprint explains several of the design's constraints, including the cap and the active-user filter.

Each timeline is a short capped list of IDs, a few kilobytes in memory. Multiplied by hundreds of millions of active users, that footprint is why timelines are capped and dormant users are dropped.
A home timeline is a Redis list of tweet IDs. Each entry is small, on the order of a tweet ID plus a little associated metadata such as the author ID, so call it around 16 to 20 bytes per entry. Capped at roughly 800 entries, one user's timeline is on the order of 15 kilobytes. That is trivial for one user and enormous in aggregate: hundreds of millions of active users at 15 kilobytes each runs into the terabytes of RAM, spread across a large sharded Redis fleet. This aggregate cost is the reason for two decisions that otherwise look arbitrary. The cap exists because an uncapped timeline would grow without bound for active users and multiply that terabyte figure many times over, for entries no one will ever scroll to. The active-user filter exists because materializing timelines for accounts that never log in would spend that expensive RAM on data that is never read. When a dormant user returns, their timeline is absent and is rebuilt by reading their follow list and pulling recent tweets from those accounts, essentially running the pull strategy once to repopulate the cache, after which push takes over again. The timelines are sharded by user ID so that any one user's timeline lives on a known instance and one hot user does not concentrate load unevenly, and the fleet is replicated so that losing an instance does not lose the timelines it held. That replication matters more than it might seem, which brings us to what happens when it is not enough.
A failure mode: cache loss and the mega-event
Two failure modes are worth naming because they are specific to this architecture and they teach the design's most important safety property: the timeline cache is not the source of truth.

A lost timeline shard is rebuilt from the durable tweet store and the follow graph, not restored from a backup of the cache. A mega-event is absorbed by the fanout queue, which trades delivery latency for stability.
The first failure mode is timeline cache loss. A Redis shard fails, or the whole fleet is cold after a deploy or an outage, and a swath of precomputed timelines simply vanishes. This is survivable precisely because those timelines were derived data, never authoritative. The system rebuilds a lost timeline the same way it builds a returning dormant user's timeline: read the user's follow list from the social graph, pull recent tweets from those accounts out of the durable tweet store, merge them in ID order, and repopulate the Redis list. It is more expensive than a cache hit, so a mass rebuild after a cold start is a load spike to plan for, but no user data is lost because the tweets and the graph are both durable and the timeline is only ever a materialized view of them. The second failure mode is the mega-event, a moment when a globally watched event drives tweet rates ten or fifteen times above normal at the same instant that everyone opens the app. Fanout work spikes on the write side while timeline reads spike on the read side. The fanout queue is what keeps this from cascading: it absorbs the burst of tweets and lets fanout workers drain it as fast as they can, which means delivery latency degrades gracefully, tweets take a few extra seconds to reach all timelines, instead of the fanout fleet falling over and dropping deliveries. The design accepts temporary staleness under extreme load as the price of never losing a tweet and never going fully down. Choosing which property to sacrifice, and choosing freshness rather than durability or availability, is the kind of deliberate tradeoff that only shows up when you have named your constraints in advance.
From reverse-chronological to ranked
Everything so far assumes the timeline is ordered strictly by time, which is how Twitter worked for years and is still the simplest correct answer. Modern timelines are ranked rather than purely chronological, and ranking layers onto the fanout architecture without replacing it, which is worth understanding because it shows the architecture's flexibility.

Ranking does not replace fanout. It sits on the read path as a scoring and reordering stage over a larger candidate set that fanout and pull still assemble.
A ranked timeline changes what happens after the candidate tweets are gathered, not how they are gathered. Fanout and pull still assemble the raw set of tweets you could see, and Snowflake IDs still keep them in a known order. On top of that, the read path gains stages. It gathers a larger candidate pool than a single screen, from your follows and sometimes from accounts you do not follow, then scores each candidate with a model that predicts how likely you are to engage with it, using signals like recency, the author's relationship to you, and the tweet's early engagement. It then reorders by score and applies heuristics that pure scoring misses, such as not showing you six tweets from the same author in a row and mixing in content types. The important architectural point is that ranking is a read-time transformation over a candidate set that the fanout system still produces. The expensive precompute-on-write inversion that makes the timeline affordable is untouched; ranking is an additional, heavier read-path stage that trades some latency and compute for relevance. This is why the fanout design has outlasted the shift from chronological to ranked feeds. Fanout answers the question of how to assemble the candidates cheaply at scale, and ranking answers the separate question of what order to show them in, and keeping those two concerns separate is what let the system evolve without a rewrite.
The patterns that transfer
Strip away tweets and follows and the timeline is a specific, reusable answer to a general class of problems, and the reason it is worth this much study is that its lessons show up far beyond Twitter.

Four decisions from this design, precompute for the read ratio, split strategy by the shape of the data, store references not copies, and keep derived state rebuildable, transfer to any feed or fanout system.
Four moves transfer directly. First, when reads dominate writes, precompute the read. The timeline exists because it is cheaper to do work once per write than repeatedly per read when reads outnumber writes by fifty to one, and this is the logic behind materialized views, denormalized read models, and caches everywhere. Second, do not pick one strategy when your data has two populations. The hybrid works because followers follow a power law, and the same split, handle the common case one way and the rare heavy case another, applies any time a distribution has a fat head and a long tail. Third, store references rather than copies of shared data. Timelines hold tweet IDs, not tweet bodies, so edits and deletes happen in one place and fan-out state stays cheap and correct, which is the same reasoning behind normalized keys and content-addressed storage. Fourth, keep your fast-path state derived and rebuildable. The timeline cache can be lost and reconstructed from durable sources, which is what makes it safe to keep in volatile memory at all, and the same discipline lets you treat any cache or index as disposable rather than precious. Learn to recognize which of these levers your own bottleneck needs and the timeline stops being a party trick about Twitter and becomes a method you can apply to the next feed, notification system, or activity stream you are handed.
I teach system design this way, from first principles with real diagrams and the tradeoffs that only surface in production, as an interactive course at systemdesign.academy. The foundation lessons are free and need no signup.
Read the free lessons: https://systemdesign.academy
Top comments (0)