Skip to content

The finished design, decision by decision

How to design a News Feed (Twitter Timeline)

Not the one correct diagram, but a design you can defend under these constraints: the finished architecture, then every stage's question with the reasoning that answers it, the tradeoffs it accepts, and where another engineer could land differently.

A home timeline at 300,000 reads a second

The short answer

9 parts, each with one job. The map below shows how requests and data move between them; the stages after it explain why each part is there.

Authors
Post messages.
Write API
Stores the post, then enqueues a fan-out job and returns.
Post store
Every post, by ID, with a cache in front for hydration.
Fan-out queue
Durable queue of fan-out jobs.
Fan-out workers
Insert the post ID into each active follower's timeline; skip accounts above the size threshold.
Social graph
Who follows whom, and who has been active recently.
Timeline cache
Per-user list of the latest 800 post IDs, three replicas. Derived; rebuildable.
Timeline service
Reads a page of IDs, merges large accounts' recent posts, hydrates and filters.
Readers
Open the app and scroll.
123456789CLIENTAuthorsSERVICEWrite APIDATABASEPost storeQUEUEFan-out queueWORKERFan-out workersSERVICESocial graphCACHETimeline cacheSERVICETimeline serviceCLIENTReaders

Select a component to see what it is responsible for and which state it owns.

  1. 1Authors → Write API: Post
  2. 2Write API → Post store: Store post
  3. 3Write API → Fan-out queue: Fan-out job
  4. 4Fan-out workers → Fan-out queue: Take jobs
  5. 5Fan-out workers → Social graph: Active followers
  6. 6Fan-out workers → Timeline cache: Push ID, trim to 800
  7. 7Readers → Timeline service: GET home timeline
  8. 8Timeline service → Timeline cache: Page of IDs
  9. 9Timeline service → Post store: Hydrate; large accounts' recent posts
  • Request / response
  • Asynchronous

Why does this design work?

Reads outnumber writes about 50 to 1, so the multiplication between authors and readers happens once, at write time: each post's ID is pushed asynchronously into every active follower's in-memory timeline, and a read is a single list lookup plus hydration.

The design bounds its worst cases. Accounts with enormous audiences are merged at read time instead of fanned out, so no single post can stall delivery. Timelines hold references, not copies, so deletes and edits take effect everywhere through hydration. Timelines are derived data, so they can be skipped for inactive users, evicted, and rebuilt after failures. Ranking, when added, runs on the read path over candidates the fan-out has already gathered.

Invariants, and where they are enforced

  • Posting never waits for delivery.

    The post is stored and a fan-out job enqueued; delivery happens asynchronously. Enforced by Write API, Fan-out queue.

  • A timeline is never the only copy of anything.

    Timelines hold post IDs; content is hydrated from the post store, which drops deleted posts. Enforced by Timeline cache, Timeline service.

  • A post costs at most a bounded amount of fan-out work, however large the audience.

    Accounts above a follower threshold are not fanned out; their recent posts are merged at read time. Enforced by Fan-out workers, Timeline service.

What does it rely on?

  • Reads vastly outnumber writes.
  • Few accounts have very large audiences, and each reader follows few of them.
  • A few seconds of delivery delay is acceptable.
  • Post IDs sort by time.

What tradeoffs does it make?

ChoiceGainsCosts
Fan-out on writeOne lookup per read.Write amplification and terabytes of memory.
Hybrid for large accountsBounded write cost; no delivery stalls.Two read paths and a merge per request.
IDs in timelinesTiny entries; deletes apply everywhere.A hydration step on every read.
Skip inactive usersFar less wasted work and memory.A slower first load when they return.

What are the reasonable alternatives?

Pure fan-out on read
Better when writes are frequent relative to reads, or each reader follows few sources.
Pure fan-out on write
Better when audiences are bounded, as in group chats or team feeds.
Read-time candidate generation from many sources
Better when the feed is mostly recommended content from outside the reader's follows.

When does it stop working?

  • Follow graphs become much denser, so average fan-out grows by orders of magnitude.
  • The product needs strict real-time delivery (sub-second) to every follower.
  • Most feed content comes from accounts the reader does not follow.

Every stage, decided and explained

Spoilers, for the whole investigation: each stage's question and its answer, the reasoning behind it, and the tradeoffs it accepts. If you have not worked through the stages yet, you may want to do that first.

Work through the stages

Stage 1 of 9 · Model

What the numbers say

Twitter reported about 30 billion timeline deliveries a day from about 400 million posts. Each timeline entry needs about 20 bytes: a post ID, the author's ID and a few flag bits.

What you need to know first

Fan-out is the multiplication of one event into many deliveries: one post, many followers' timelines. The work can happen at write time (when the post is made) or at read time (when a timeline is loaded). See Fan-out on write and fan-out on read.

Which is cheaper depends on how often each side happens. Work belongs on the rarer side.

300,000 timeline reads a second, and about 6,000 posts a second at peak. Roughly how many reads per post?

About 50 reads per post.

300,000 ÷ 6,000 = 50. Every unit of work moved from read time to write time is paid 50 times less often.

30 billion timeline deliveries a day from 400 million posts. On average, how many timelines does each post reach?

About 75 timelines.

30,000,000,000 ÷ 400,000,000 = 75. That's an average over a very uneven distribution: most accounts have hundreds of followers, a few have tens of millions.

Each user's timeline keeps 800 entries of about 20 bytes. About how many terabytes for 150 million users (one copy)?

About 2.4 TB.

800 × 20 = 16 KB per user. 16 KB × 150,000,000 = 2.4 × 10¹² bytes ≈ 2.4 TB, about 7 TB with three replicas. A big memory fleet, but an affordable one.

What the stage asks

Which statements follow?

  1. Holds

    Timeline reads outnumber posts by roughly 50 to 1.

    300,000 reads a second against about 6,000 posts a second (including peaks) is about 50:1. Work moved from reads to writes is multiplied by far fewer events.

  2. Holds

    On average, each post is delivered to about 75 timelines.

    30 billion deliveries / 400 million posts ≈ 75. That is the average; the distribution has an extremely long tail.

  3. Fails

    Assembling timelines at read time would cost about the same as fanning out at write time.

    At read time, each of 300,000 requests a second would fetch recent posts from the hundreds of accounts its reader follows and merge them: tens of millions of lookups a second plus a merge per request. At write time, ~350,000 cheap list inserts a second on average do the same job once.

  4. Fails

    Keeping 800 post IDs for each of 150 million users needs petabytes of memory.

    800 × ~20 bytes ≈ 16 KB a user; 150 million users ≈ 2.4 TB, about 7 TB with three replicas. A large in-memory fleet, but nowhere near petabytes.

The reasoning

  1. Reads outnumber posts about 50:1, so work moved to write time is paid far less often.
  2. The average post reaches about 75 timelines, but the distribution has a very long tail.
  3. Timelines of IDs fit in a few terabytes of memory.

Two numbers decide the design: reads are about 50 times more frequent than writes, and the average post goes to about 75 timelines. Moving the multiplication to write time is the cheaper side for the average post, and keeping the result in memory is affordable.

The word average is doing a lot of work there. The tail of that distribution is the subject of stage 4.

Stage 2 of 9 · Decide

When does the multiplication happen?

Somebody has to combine "one author" with "many readers". Choose where.

What you need to know first

The two strategies:

Fan-out on write (push)Fan-out on read (pull)
Postinginsert into every follower's liststore the post once
Readingfetch one listfetch every followed account's posts, merge
Cost lands onwritesreads

With pull, each of 300,000 reads a second fetches recent posts from the ~200 accounts its reader follows. About how many lookups a second is that?

About 60 million lookups per second.

300,000 × 200 = 60 million lookups a second, plus a merge per request. Push does about 75 inserts per post × 5,000 posts a second ≈ 375,000 inserts a second for the same result.

Timelines could hold full copies of posts or just post IDs. What happens to copies when the author deletes a post?

Every copy stays in every timeline unless another fan-out finds and removes them.

Copies are independent. With IDs, the post lives once; deleting it there makes it disappear from every timeline when IDs are turned back into posts at read time.

What the stage asks

How should home timelines be built?

  1. Flawed

    At read time, with one SQL query joining follows to posts, ORDER BY created_at DESC LIMIT 50

    This is how most products start, and it is fine at small scale. At 300,000 requests a second, each merging hundreds of authors' posts across a sharded post store, it misses the latency requirement by orders of magnitude.

  2. Defensible

    At read time: fetch each followed account's recent posts from a per-author cache and merge them

    Posting is O(1) and there is nothing to precompute. But every read fans out to hundreds of authors and merges, so the cost lands on the side that happens 50 times more often. It is the right shape when reads are rare relative to writes.

  3. Sound

    At write time: insert the new post's ID into each follower's precomputed timeline list in memory; a read fetches one list

    Reads become a single lookup of a short list, which is exactly what you want 300,000 times a second. Each post costs one insert per follower, done asynchronously so the author is not kept waiting. Storing IDs keeps entries tiny and leaves the content in one place.

  4. Flawed

    At write time: copy the full post into each follower's timeline

    Every post is stored 75 times on average, and editing or deleting it needs another fan-out to find and fix every copy. Deleted posts would keep showing up, which violates a requirement.

What a strong answer covers

  • Uses the read/write ratio: precomputing moves work to the much rarer side.
  • Fan-out runs asynchronously, so posting stays fast.
  • Timelines store IDs, not copies, so content lives in one place.
  • Anticipates that very large audiences make write-time fan-out expensive.Supporting

The reasoning

  1. Put the fan-out on the rarer side: here, write time.
  2. Run fan-out asynchronously so posting stays fast.
  3. Store IDs in timelines, not copies, so content lives in one place.

Twitter's home timeline is fan-out on write: each user's timeline is a list in a Redis cluster, capped at 800 entries and replicated three times, and a post is inserted into every follower's list. Reads are one list lookup plus hydration. See Fan-out on write and fan-out on read.

Timelines hold IDs (plus the author and a few flags). The post itself lives once, in the post store, which keeps timelines tiny and makes deletion a single write.

Stage 3 of 9 · Model

Trace a post to a follower's screen

An author with 200 followers posts. One of those followers opens the app a few seconds later.

What you need to know first

Tracing a request means following it through each component in order, and noting where the user stops waiting. Everything after that point can be slow or retried without them noticing, but it's also why there's a delay before followers see the post.

With fan-out on write done asynchronously, how long does the author wait after pressing Post?

Only for the post to be stored and a fan-out job enqueued

The author's request ends there. Delivering to followers happens afterwards, which is why it can take a few seconds.

Hydration turns IDs back into content: the read path fetches a page of post IDs from the timeline, then looks up the posts themselves (mostly from a cache). Posts that no longer exist are simply dropped.

What the stage asks

Put these steps in order.

In this order

  1. 1The write API stores the post durably and assigns its ID
  2. 2A fan-out job is enqueued and the author's request returns
  3. 3A fan-out worker asks the social graph for the author's recently active followers
  4. 4For each follower, it pushes the post ID onto their timeline list and trims the list to 800
  5. 5The follower's app asks the timeline service for the first page
  6. 6The service reads a page of IDs and hydrates them into posts, dropping deleted ones

The author waits only for the first two steps. Everything after the enqueue happens asynchronously, which is why a post can take a few seconds to reach everyone. The follower's read touches only their own list and the post cache: no other user's data, no merge, no scan.

The reasoning

  1. The author waits only for the post to be stored and the fan-out job enqueued.
  2. The write path costs O(followers) asynchronously; the read path costs O(page size).
  3. Reads hydrate IDs into posts, which is where deletions take effect.

Tracing makes the two paths visible: a write path that pays O(followers) asynchronously, and a read path that pays O(page size). Every later stage changes one of these two paths.

Stage 4 of 9 · Break it

Thirty million followers

Twitter's target was to deliver to a million followers in about 3.5 seconds. Thirty million is a different problem.

What you need to know first

Averages hide the units of work that hurt. A system that's fine for a 200-follower account can stall on a 30-million-follower one, because that single post becomes a single enormous job, competing with every other delivery.

Fan-out delivers about 1 million timelines in 3.5 seconds. About how many seconds for one post to 31 million followers?

About 108 seconds.

31 × 3.5 ≈ 108 seconds, almost two minutes for one post, during which the workers are busy with it and everyone else's deliveries wait.

Pull has the opposite profile: posting is one write, and readers pay a merge. For an account with millions of followers, the merge is cheap: its recent posts are the same for every reader and sit in cache.

So the strategy can be chosen per account: push for the many accounts with ordinary audiences, pull for the few very large ones.

A reader follows 3 accounts above the threshold. What extra work does their timeline read do?

Fetch those 3 accounts' recent posts (cached) and merge them into their list by time

A handful of cached lookups and a small merge, bounded by how many large accounts a person follows, which is usually few.

What the stage asks

What do you change?

  1. Defensible

    Add more fan-out workers

    More throughput shortens the backlog, but each such post is still 31 million inserts arriving at once, competing with everyone else's deliveries. You would be provisioning for the rare spike from a few accounts.

  2. Sound

    Don't fan out posts from accounts above a follower threshold; merge their recent posts into each reader's timeline at read time

    A huge account's post costs one write. Readers who follow a few such accounts pay a small merge on read: fetch those accounts' latest posts (which are heavily cached, since everyone wants them) and interleave by ID. Twitter described moving to exactly this hybrid.

  3. Flawed

    Process the celebrity's fan-out ahead of everyone else's

    Her followers get the post sooner, and every other post in the system waits behind 31 million inserts. It makes the delay worse for everyone else.

  4. Flawed

    Rate-limit how often very large accounts may post

    It changes the product to fit the architecture, and even one post is still 31 million inserts.

What a strong answer covers

  • One post from a huge account becomes millions of writes that delay everyone.
  • Few accounts are that large, and each reader follows few of them, so read-time merging is cheap.
  • Those accounts' recent posts are hot and cacheable.
  • Mentions choosing the threshold, or the extra merge latency on reads.Supporting

The reasoning

  1. One post to tens of millions of followers is a single huge job that delays everyone else's delivery.
  2. Choose fan-out per account: push for ordinary audiences, merge very large accounts at read time.
  3. Bound the worst-case unit of work on the write path.

Neither pure strategy survives a long-tailed follower distribution. Push is right for the many and wrong for the few; pull is the reverse. The hybrid gives each account the strategy that suits its audience size. See Fan-out on write and fan-out on read.

Notice the shape of the fix: it bounds the worst case of the write path, the same move as bounding partitions or rejecting oversized work. A system is only as smooth as its largest single unit of work.

Tradeoffs

ChoiceGainsCosts
Merge large accounts at read timeBounded write cost; no delivery stalls.Every read does a small merge; two code paths to keep consistent.

Stage 5 of 9 · Decide

Timelines nobody reads

Many accounts have not opened the app in months, yet every post from someone they follow is still inserted into their timeline. Twitter stopped fanning out to users who had not logged in for 30 days.

What you need to know first

Derived data is computed from other data and can be rebuilt from it. A timeline is derived: it's a function of the post store and the follow graph.

Anything derived can be skipped, evicted or lost without losing anything durable. The cost is the work to rebuild it when it's needed again. See Soft state.

Fan-out skips users inactive for 30 days. One of them returns. What do they see?

A full timeline, rebuilt from their follows on that first request, which is slower than usual

The first load does a read-time merge to rebuild the list; after that, fan-out keeps it current again.

Timelines can be rebuilt, yet Twitter kept three replicas of each. Why?

Not to protect data, which is safe in the post store, but to avoid a rebuild storm. Losing a node with millions of timelines and rebuilding them all at once would flood the post store and graph with read-time merges. Replicas make node loss cheap.

What the stage asks

Which statements hold?

  1. Fails

    Users skipped by fan-out will see an empty timeline when they return.

    On their return, the timeline is rebuilt from the people they follow and the post store (a one-off fan-out on read), then kept up to date by fan-out again.

  2. Holds

    A returning user's first load is slower than usual.

    That one request does a read-time merge. One slow load per returning user is a much better deal than inserts into millions of timelines nobody opens.

  3. Fails

    If a timeline cache node is lost, posts are lost.

    Timelines are derived: every entry can be recomputed from the post store and the graph. Losing a node costs rebuild work, which is why Twitter kept three replicas: not to protect data, but to avoid a rebuild storm.

  4. Holds

    Letting unread timelines expire from memory saves space without losing anything durable.

    Anything that can be rebuilt can be evicted. See Soft state.

The reasoning

  1. Timelines are derived data: skip, evict or lose them and rebuild from the post store and graph.
  2. Stop fanning out to inactive users; rebuild once when they return.
  3. Replicas of derived data protect against rebuild storms, not data loss.

Once timelines are understood as derived data, a whole class of decisions gets easier: skip them for inactive users, evict them under memory pressure, rebuild them after failures. The durable truth is the post store and the graph; the timeline cache is an optimisation that can always be reconstructed.

That only holds because entries are references. If timelines held the only copy of anything, none of these moves would be safe.

Stage 6 of 9 · Break it

The post that wouldn't die

Find every line that contributes to either problem, or would fall over on a large account.

What you need to know first

A Redis list used as a capped timeline needs two operations per insert: LPUSH adds the newest entry at the front, and LTRIM key 0 799 drops everything past 800. Without the trim, lists grow forever.

Sending both in one pipeline (many commands in one round trip) keeps it cheap.

A worker awaits each insert separately, at about 1 ms per round trip. How many minutes for an account with 1 million followers?

About 17 minutes.

1,000,000 × 1 ms = 1,000 s ≈ 17 minutes. Pipelining hundreds of inserts per round trip brings it down to seconds.

Timelines store IDs. Where does a deleted post get removed from what followers see?

At hydration: the read looks up each ID, and a deleted post is no longer found

One delete in the post store takes effect for every timeline the next time it's read. No second fan-out needed.

What the stage asks

Select the faulty lines.

Codetypescriptfanout.ts and timeline.ts
  1. 1async function fanOut(job: FanOutJob) {
  2. 2 const post = await posts.get(job.postId);
  3. 3 const followers = await graph.followers(post.authorId);

    Fetches every follower, including inactive ones and audiences of millions, in one call. It should page through active followers only (and skip accounts over the threshold).

  4. 4 for (const followerId of followers) {
  5. 5 await redis.lpush(`timeline:${followerId}`, JSON.stringify(post));

    Pushes a full copy of the post, so a delete or edit never reaches the timelines. Push the post ID (and author ID) instead.

  6. 6 }

    No LTRIM after the push: lists grow without bound, which is the climbing memory. Trim each list to 800 entries, in the same pipeline.

  7. 7 await job.ack();
  8. 8}
  9. 9
  10. 10async function homeTimeline(userId: string, page = 0) {
  11. 11 const raw = await redis.lrange(`timeline:${userId}`, page * 50, page * 50 + 49);
  12. 12 return raw.map((s) => JSON.parse(s));

    Returns the stored copies directly. The read path should hydrate IDs from the post store, which is where deletions take effect.

  13. 13}

What the fix has to do

  • Store references (IDs) in timelines and hydrate on read, so deletes and edits apply everywhere at once.
  • Cap each list (LTRIM to 800) as part of the insert.
  • Page through followers and pipeline the inserts in batches rather than one awaited round trip each.
  • Fan out only to active followers, and skip huge accounts.Supporting

The reasoning

  1. Store IDs and hydrate on read so a delete takes effect everywhere at once.
  2. Trim each timeline as part of the insert, or lists grow without bound.
  3. Page through active followers and pipeline inserts instead of one awaited round trip each.

Copies are the root of the deletion bug: a derived view that contains content must be updated whenever the content changes, and nobody had written that second fan-out. Storing IDs makes deletion a single write to the post store; hydration takes care of every timeline.

The awaited insert per follower is a quieter problem: at a millisecond per round trip, a million followers is seventeen minutes. Batch and pipeline.

Stage 7 of 9 · Break it

Write the merged read

Implement the read path for the hybrid design: the reader's precomputed list, plus the recent posts of any very large accounts they follow, merged newest first. Post IDs are time-sortable, so a larger ID is a newer post. Support a before cursor for the next page.

What you need to know first

Post IDs here are time-sortable: a larger ID is a newer post (Twitter's Snowflake IDs start with a timestamp). That makes merging sources a sort by ID, and pagination a cursor: "give me posts with ID less than X". See Generating unique identifiers.

Two 64-bit IDs as decimal strings: "999" and "1000". Compared as strings, which is 'larger'?

"999", because '9' sorts after '1'

String comparison goes character by character. IDs of different lengths sort wrongly, so compare them as numbers. For 64-bit values in JavaScript, that means BigInt.

A page merges the reader's list with three large accounts' posts. Why must the same 'before' cursor be applied to every source?

So every source contributes only posts older than the last one the client saw. If one source ignored the cursor, its newest posts would show up again on every page; if the cursor were applied unevenly, posts would be skipped.

What the stage asks

Implement homeTimeline.

Reference implementation

const newestFirst = (a: string, b: string) => (BigInt(b) > BigInt(a) ? 1 : BigInt(b) < BigInt(a) ? -1 : 0);

export async function homeTimeline(userId: string, before: string | null, limit = 50): Promise<Post[]> {
  const [own, large] = await Promise.all([
    timelineIds(userId, before, limit),
    largeAccountsFollowed(userId),
  ]);
  const fromLarge = await Promise.all(large.map((a) => recentPostIds(a, before, limit)));

  const page = [...new Set([...own, ...fromLarge.flat()])].sort(newestFirst).slice(0, limit);
  const posts = await hydrate(page);
  return page.flatMap((id) => {
    const post = posts.get(id);
    return post ? [post] : [];
  });
}
// The client asks for the next page with before = the last ID it received.
  • Each source contributes at most limit IDs older than the cursor, so the merged page is correct even if all of it comes from one source.
  • IDs are compared as numbers. Two 64-bit IDs as decimal strings only compare correctly as strings if they have the same number of digits.
  • Deleted posts disappear at hydration, so the page may be slightly shorter than limit. Clients handle that by asking again from the last ID returned; over-fetching a little avoids most short pages.
  • The large accounts' recent posts are the hottest keys in the system, which is fine: they are the same for every reader and cache perfectly.

What a strong answer covers

  • Fetches the precomputed page and the large accounts' recent posts in parallel.
  • Merges by ID (time), newest first, de-duplicating, and takes only the page size.
  • Applies the same 'before' cursor to every source so pages do not overlap or skip.
  • Hydrates IDs and drops posts that no longer exist (deleted).
  • Compares 64-bit IDs numerically (BigInt), not as strings of different lengths.Supporting

The reasoning

  1. Time-sortable IDs make merging sources a sort, and pagination a single cursor.
  2. Apply the cursor to every source and take only the page size after merging.
  3. Compare 64-bit IDs numerically, and drop posts that fail to hydrate.

The hybrid's cost lives here: a few extra cached reads and a merge. Because IDs encode time, merging sources is a sort on IDs, and pagination is a single cursor that every source understands. See Generating unique identifiers.

Stage 8 of 9 · Change it

Top posts first

Rankings depend on the reader, the post, and signals such as replies and likes that arrive over the following minutes and hours.

What you need to know first

A ranked feed has two steps:

  1. Candidate generation: what could this reader see? (Posts from accounts they follow, maybe more.)
  2. Ranking: in what order? (Scored using the reader, the post, and engagement signals.)

The first changes only when posts or follows change. The second changes every minute as likes and replies arrive.

Scores are computed at fan-out time and stored with each timeline entry. A post gets most of its likes in the hour after it's published. What's wrong?

The score was computed before the engagement existed, so it's stale within minutes. Updating it means fanning out again to every follower's list, continuously. Ranking has to happen when the signals are known: at read time.

Ranking at read time scores each candidate. What keeps that affordable at 300,000 reads a second?

The candidate set is small (a few hundred precomputed IDs), so each request scores a bounded number of posts.

Fan-out already did the expensive 'who could see this' work. Ranking only orders what it produced.

What the stage asks

How do you adapt the design?

  1. Sound

    Keep fan-out as candidate generation; at read time, score the candidates (precomputed list plus merged sources) and sort by score

    Fan-out still does the expensive 'who could see this' work ahead of time; ranking runs on a few hundred candidates per request with the latest signals. Ranking is a read-path concern because the signals change after the write.

  2. Flawed

    Compute each post's score at fan-out time and store timelines sorted by score

    Scores computed at write time are stale within minutes, because engagement arrives afterwards. Re-ranking would mean fanning out again, continuously, to millions of lists.

  3. Defensible

    Drop precomputed timelines; at read time gather every post from followed accounts in the last few days and rank them

    Maximally flexible, and it simplifies the write path, but it brings back the read-time scatter-gather that the numbers ruled out, now with a ranking model on top. Large ranked feeds do generate candidates from several sources at read time, but they lean heavily on precomputed and cached sources to make that affordable.

What a strong answer covers

  • Ranking signals change after the post is written, so scores must be computed at read time (or refreshed).
  • Precomputed timelines become cheap candidate generation for the ranker.
  • Ranking is bounded by the candidate set size, not the whole graph.

The reasoning

  1. Fan-out answers 'what could this reader see?'; ranking answers 'in what order?'.
  2. Rank at read time, because engagement signals arrive after the post is written.
  3. Precomputed timelines become cheap, bounded candidate generation.

A useful way to see the change: fan-out answers "what could this reader see?" and ranking answers "in what order?". The first is stable once a post is written and benefits from precomputation; the second changes by the minute and belongs on the read path. Separating the two keeps both cheap.

Stage 9 of 9 · Defend it

Defend the hybrid

Your interviewer: "You have two read paths, a queue doing a third of a million inserts a second, and terabytes of Redis. Wouldn't fan-out on read with a good cache be simpler?"

What you need to know first

"Wouldn't X be simpler?" deserves a numeric answer. Restate the ratio that drove the design, show what the alternative costs in the same units, concede the real costs of yours, and say what would change your mind.

"Use pull with a good cache." Why doesn't caching rescue pull here?

Each reader's merged timeline is different, so the expensive merge result can't be shared between readers.

You can cache each author's recent posts, but the merge across ~200 authors still runs for every read, 300,000 times a second.

What the stage asks

Defend the design with numbers, concede what is true in the challenge, and say what would make you switch.

Reference answer

The numbers. Reads are about 50× writes, and the average post reaches ~75 timelines. Precomputing costs roughly 350,000 list inserts a second; pulling would cost tens of millions of lookups a second plus a merge on every one of 300,000 requests.

Why a cache doesn't rescue pull. You can cache each author's recent posts, and the hybrid does exactly that for large accounts. But the expensive part of pull is the per-reader merge across hundreds of authors, and every reader's merge is different, so its result is not shared. The precomputed timeline is the cached merge.

What I concede. There are two read paths to keep consistent; the cache holds terabytes; a lost node means rebuilding timelines; and a few seconds' delay before followers see a post. All of it is bounded: the hybrid caps write cost, timelines are derived and rebuildable, and replicas avoid rebuild storms.

When I would switch. If reads per post fell sharply (a write-heavy product), if follow graphs became much denser so the average fan-out exploded, or if ranking moved to mostly out-of-network content, candidate generation at read time would win.

What a strong answer covers

  • Uses the ratio and fan-out numbers to show which side should pay.
  • Explains why caching does not rescue pull: each reader's merge is different, so the merged result is not shared.
  • Concedes the real costs: two read paths, memory, and rebuild work after failures.
  • Names conditions under which pull (or a different split) would win.

The reasoning

  1. Defend with numbers: the read/write ratio decides which side pays.
  2. Caching doesn't save pull, because every reader's merge is different.
  3. Concede the costs (two read paths, memory, rebuilds) and name what would make pull win.

"Simpler" is a fair objection. Answer it by showing where the work goes under each design, and by naming what you would need to see to change your mind. That is what turns a design preference into an engineering argument.

How you did

Now try it as an interview question

  • “Design Twitter.”
  • “Design the Facebook or Instagram news feed.”
  • “Fan-out on write or fan-out on read? Defend your choice.”
  • “How do you handle a celebrity with 50 million followers in your feed design?”

The interview mode mixes stages from this and other investigations with concept recall and questions about your own projects.

Back to the last stage