Skip to content

Twitter and the feed

Fan-out on write against read, the celebrity problem, ranking, and the graph underneath it all.

01

Fan-out on write against fan-out on read

intermediate

A home timeline is the union of everything the accounts you follow have posted, most recent first. There are exactly two places that union can be computed, and the entire design follows from which one you pick.

Fan-out on read computes it when the timeline is requested: look up who this person follows, fetch recent posts from each, merge, sort. Writing is trivial, a single row. Reading is expensive and gets worse the more accounts someone follows, and it happens far more often than writing.

Fan-out on write does the work at post time: when someone posts, push the post's identifier into a precomputed list for every follower. Reading becomes a single lookup of an already-assembled list, which is exactly what you want for the operation that dominates. The cost is that one post becomes as many writes as the author has followers.

The ratio decides it. Timelines are read vastly more often than posts are written, so paying at write time to make reads cheap is the right trade for almost everyone. Twitter maintains materialised timelines of roughly the most recent 800 post identifiers per user, held in a Redis-derived store where pushing to the head and trimming the tail are both cheap operations, which is precisely the access pattern fan-out needs.

Note what is stored: identifiers, not posts. Eight hundred identifiers per user is a manageable amount of memory; eight hundred full posts per user, duplicated across every follower, would not be. The timeline is an index into the posts, and the posts themselves are fetched and cached separately.

Check yourself

Why does fan-out on write suit a timeline despite costing far more writes?

one writeone push per followeridshydrateAuthor postsPost servicestores oncePost storethe contentFan-out workerreads the followerlistTimeline store800 ids per userReaderone lookup
ClientServiceData storeQueue / streamDashed = asynchronous
Pay at write time so the read is one lookup
Evidence · 1 source
  1. 01
    Twitter engineering, via High Scalability: the architecture Twitter uses for 150M active users

    That home timelines are materialised in a Redis cluster holding roughly the most recent 800 tweet identifiers per user, and that identifiers rather than tweets are what is stored.

Ask about this

Answers are generated and can be wrong. The topic above is the reviewed version.

02

The celebrity problem, and the hybrid that solves it

advanced

Fan-out on write has one failure mode and it is severe. An account with fifty million followers posts once, and that single action becomes fifty million writes. Post ten times in a day and it is five hundred million. The work is unbounded in the follower count, and a handful of accounts can saturate the infrastructure that everyone else depends on.

Worse, it is bursty in exactly the wrong way. Popular accounts post at moments of high general activity, so the fan-out spike lands while read traffic is also peaking. A queue absorbs some of it, but a large enough fan-out delays every other user's timeline behind it.

The answer is a hybrid, and it is what Twitter, Instagram and Facebook all independently arrived at. Ordinary accounts fan out on write, because their follower counts are small and the cost is trivial. Accounts above a threshold, commonly cited around ten thousand followers, do not fan out at all. Their posts are fetched at read time instead.

So a timeline read does two things and merges them: fetch the precomputed list, and separately fetch recent posts from the small number of large accounts this person follows. Merge by time, return the top slice. The pull side is bounded because nobody follows very many celebrities, and the push side is bounded because ordinary accounts have few followers. Each strategy is applied exactly where its cost is small.

The threshold itself is an operational dial rather than a principle. Too low and too many accounts are pulled, making every read do more work. Too high and the write spikes return. It also creates a boundary that has to be handled: an account crossing the threshold needs its existing followers' timelines to stay coherent while the strategy changes underneath them.

Check yourself

What is the risk in setting the celebrity threshold too low?

fan out on writeno fan-outfetched per readOrdinary accountfew followersLarge accountmillionsFan-out workerpush pathRecent postspull pathStored timelineids per userMerge at readsort by timeReader
ClientQueue / streamData storeServiceDashed = asynchronous
Push for the many, pull for the few, merge at read
Ask about this

Answers are generated and can be wrong. The topic above is the reviewed version.

03

Ranking: when the timeline stops being chronological

advanced

A reverse-chronological timeline is simple, predictable, and increasingly useless as the number of accounts someone follows grows. Follow two thousand accounts and the last hour contains more posts than anyone will read, so the question shifts from what is newest to what is worth showing.

Ranking changes the shape of the system in a way that is easy to underestimate. Chronological order can be maintained incrementally: a new post goes at the head, and nothing else moves. A ranked order can change for a post that already exists, because it gained engagement, or because the reader's interests shifted, or because the model was retrained. The timeline is no longer an append-only list.

The usual resolution is to keep fan-out producing a candidate set in time order, then rank that candidate set at read time. Retrieval stays cheap and incremental; scoring happens on a few hundred candidates rather than everything ever posted. This is the same retrieve-then-rank shape that search engines use, and for the same reason: scoring is too expensive to apply to the full corpus.

The costs are worth naming honestly. A ranked timeline is not reproducible, so two people cannot compare what they saw. Debugging becomes statistical rather than deterministic. And engagement-based ranking has a well-documented pull towards content that provokes, because provocation generates engagement, which means the objective function is a product decision with consequences rather than a technical parameter.

Check yourself

Why separate retrieval from ranking rather than ranking everything?

Stored timelinetime orderedLarge accountsfetched per readCandidate seta few hundredFeaturesengagement, affinityRankerscores candidatesTimelineordered by score
Data storeServiceClientDashed = asynchronous
Retrieve in time order, then score the shortlist
Ask about this

Answers are generated and can be wrong. The topic above is the reviewed version.

04

The follower graph, and why it is not just a table

intermediate

Underneath everything is a graph: who follows whom. It looks like a two-column table and behaves like nothing of the sort, because of how it is queried and how skewed it is.

Both directions are needed and they are wildly asymmetric. Fan-out asks for an account's followers, which for a large account is tens of millions of rows and is read on every post. A timeline asks who someone follows, which is usually hundreds. Storing one relationship and deriving the other means one of these queries is always expensive, so both directions are typically materialised and kept consistent.

The distribution is the real problem. Most accounts have a handful of followers, and a few have tens of millions. A single row's worth of relationship for one account and a hundred million for another cannot sit comfortably in the same partitioning scheme: shard by account and the shard holding a very large account is enormously hotter than its neighbours.

The graph also mutates constantly, and each mutation has consequences. A follow means the new follower's timeline is missing that account's history, so either it is backfilled or their timeline is simply thin until new posts arrive. An unfollow means existing entries are now wrong. Blocks and mutes add filtering that must be applied at read time, because applying them at write time would mean rewriting timelines whenever a block changes.

That last point is a useful general lesson. Some things must be evaluated at read time, not because it is cheaper but because the alternative is a rewrite of everything already written.

Check yourself

Why materialise both directions of the follow relationship?

millions for a largeaccounthundredsremoves entriesFollow or unfollowGraph servicewrites bothdirectionsFollowingwho I followFollowerswho follows meFan-outreads followersTimeline readreads followingBlocks and mutesapplied at read
ClientServiceData storeQueue / streamDashed = asynchronous
Both directions materialised, filters applied at read
Ask about this

Answers are generated and can be wrong. The topic above is the reviewed version.