Skip to content

System DesignWorked problems

Design a news feed

Design a news feed like Facebook, Instagram or X. Start with pull, let the numbers break it, then grow the design step by step through push, big accounts, ranking, paging, deletes and failures.

Mid-level30 min core · 53 min full depth

“Design the home feed for a social network. Users follow other users and see their posts.”

Everyone has used a news feed, so it looks easy. And it is easy, until you count the users.

A feed answers one question: “show me the newest posts from the people I follow.” Answering it once is cheap. Answering it when 500 million people ask ten times a day is not. So the whole problem comes down to one choice. Do we do the work when a post is written, or when a feed is read? We start with the simplest answer, and let the numbers show where it breaks. Then the design grows, one step at a time, until it survives very large accounts, ranking, scrolling, deletes and failures.

If you only have a minute, this is the design. The rest of the page explains each part.

  1. When someone posts, we copy the post’s ID into a short list for each of their followers. This list is called a timeline, and it lives in a cache. Opening the feed then means reading one timeline and fetching 20 posts.
  2. Some accounts have millions of followers. Copying one of their posts 50 million times would hold up everyone else’s posts. So we don’t copy those. We fetch their newest posts when a follower opens the feed, and mix them in.
  3. We don’t keep timelines for people who haven’t opened the app in a month. If they come back, we build their timeline once.
  4. A ranker puts the best posts first. It gets a strict time limit. If it is slow, we show the posts newest first instead, so the feed still loads.
  5. Deleted posts, blocks and privacy changes are checked every time a feed is read. Old copies of a post’s ID may still sit in many timelines, but the check hides the post anyway.

Before drawing anything, we need to know what we’re building. A few questions change the design a lot. For the rest, we can assume an answer and move on.

Interviewerminute 0

Design a news feed. Users follow other users and see their posts on a home screen.

What they are testing

Whether you ask how many followers the biggest account has. That one answer breaks the simple design.

These four questions matter most:

  • Do people follow, or are they mutual friends? They follow, like on Instagram or X. So one account can have 100 million followers. If friendships were mutual, like on Facebook, there would be a cap (Facebook’s is 5,000). Then there would be no huge accounts, and the hardest part of this problem would go away.
  • How many people, and how often? 500 million people a day, each opening the feed about ten times. At 10 million people, a much simpler design would be enough.
  • Best posts first, or newest first? Best first by default, with newest first as an option.
  • When must a delete or a block take effect? On the very next feed load. This sounds like a small detail, but it changes how we read the feed.

From these answers we make three promises. The rest of the design exists to keep them:

  • The feed always loads, in under 300 ms for 99% of requests. A feed that is a little out of date, or not ranked, still counts as loaded.
  • A new post reaches followers within 5 seconds. Usually it takes about 1 second.
  • Deleted, blocked and private posts never show, starting from the next load.

We leave out ads, recommended posts, comments, likes, notifications and search.

Go deeperThe full requirements, and five more questions2 min

These questions matter less, but each answer still moves the design a little. Here is what we assume, and what would change if the answer were different.

Question We assume If the answer were different
How fresh? 99% of followers within 5 seconds “Within a minute” lets fan-out wait and send posts in bigger groups
What content? Text, photos, short video Text only removes the CDN and blob storage, the biggest costs
Largest accounts? Over 100 million followers If nobody has more than about 100,000, copying to everyone works
Global users? About a third in each of three regions One region is simpler, with no delay copying data between regions
A cap on following? 7,500 Without a cap, building one person’s feed has no upper limit

The numbers behind the three promises:

  • Availability. Loading the feed works 99.99% of the time. Posting works 99.95% of the time. If a post fails, the app keeps it as a draft and tries again.
  • Speed. The first page takes under 300 ms on the server for 99% of loads, and under 100 ms for half of them. That includes ranking. Posting takes under 500 ms for 99% of posts.
  • Durability. When the app gets “post saved”, the post is already on 3 machines. Timelines are only a cache, so we can rebuild them.
  • Consistency. Authors see their own post at once. Followers see it within the 5-second promise. Deletes, blocks and privacy changes apply from the next load.

Now we know what to build. Next we ask how big it is. Two numbers matter most: how often people read the feed, and how often they post.

Assumptions
Daily active users
500 million
Feed loads per user a day
10
Posts per user a day
0.2
Followers per user, average
200
Followers active this month
60%
Follows of big (pulled) accounts
30%
Peak over average
3×
Timeline entries kept per user
500
Years to plan for
5 years
Feed reads58,000/s170,000/s at peak

500 million people open the feed ten times a day. That is 5 billion loads a day, or about 58,000 a second. In the evening it is three times higher.

So we reading a feed must cost only a few cache lookups, not hundreds.

Posts1,200/s3,500/s at peak

Each person posts 0.2 times a day, so 100 million posts a day. For every post, the feed is read 50 times.

So we storing posts is not the hard part. What each post sets off is.

These two numbers set the shape of the problem. People read far more than they write: 50 feed loads for every post. Keep that ratio in mind. It decides the design in the first step below.

We’ll start with the simplest feed that gives the right answer. Then we let the numbers break it. Each step fixes only what broke, adds only the boxes that fix needs, and says what the fix costs. The drawing grows with each step.

Step 1. The simplest feed: build it when it is opened

Section titled “Step 1. The simplest feed: build it when it is opened”

The simplest idea is to do nothing when someone posts, and all the work when someone reads. This is called pull. Saving a post is one write. Opening the feed works like this:

  1. The feed service asks the graph service whom you follow. That’s about 200 accounts.
  2. It reads the newest posts of each of those accounts from the post cache.
  3. It merges them by time and returns the newest 20.
News feed · architecture

Stage 1: Pull: build the feed when it is opened

PullEach load reads the newest postsof ~200 followed accounts andmerges them. Nothing is copied.UsersCDNimages, videoBlob storemedia originalsAPI gatewayauth, limitsFeed servicemerges ~200 lists567 servers, 60% busyPost servicecreate, delete3 servers, 57% busyPost storesharded by post21% busyPost cacheposts, author lists567% busyGraph servicefollow edges15 servers, 57% busyGraph storeboth directions2% busy
Color keyServiceDatabaseCacheQueue or streamBackground workerNew in this stageAsync or occasional

Stage 1 of 5

Pull: build the feed when it is opened

Why this stage

The simplest design that is correct.

Each post is saved once, and nothing is copied anywhere.

What we add

A feed service asks the graph service whom you follow, reads the newest posts of each of those accounts from the post cache, merges them by time, and returns 20.

What it costs

About 200 lookups for every feed load: 35 million a second at peak.

Every load does the same work again, even when nothing changed.

At 170k feed loads per second, Post cache is 567% busy. Stage 2 is the fix.

Pinch or tap the zoom buttons to read small labels.Drag any box to move it. Reset puts every box back.
The numbers behind the busy bars

How busy it gets

  • For every 50 feed loads, someone publishes 1 new post. Reading is the heavy side.

What one machine can do

  • A feed server builds about 5,000 feeds a second when the timeline is ready. If it has to pull about 200 lists first (stage 1), it manages only about 500.
  • One Redis node answers about 200,000 lookups a second with multi-gets.
  • One fan-out worker copies about 20,000 posts into timelines a second.
  • One ranking server ranks about 500 feeds a second, scoring 500 posts each.

How big the fleet is, per region

  • The post cache needs 30 nodes. Only 2% of reads miss it and go to the post store, which has 16 shards.
  • The timeline cache needs 300 nodes of 100 GB, about 30 TB of memory at about 30 bytes an entry.

Why push costs less than it looks

  • A post is copied to about 84 timelines, not 200. About 40% of followers are inactive and skipped, and 30% of follows point at big accounts, which are pulled instead.

This is correct, and a new post shows up at once. Before we test it against the numbers, here is where the data lives.

Three kinds of data grow at very different speeds. Over five years, posts grow to about 180 TB. The list of who follows whom grows to about 10 TB. Photos and videos grow to 37 PB. Data that grows this differently shouldn’t share one database, so each kind gets its own owner:

  • The post service owns posts. It spreads them across many machines by post ID, and keeps a post cache in front of them.
  • The graph service owns who follows whom. It stores each follow twice: once under “X’s followers” and once under “who Y follows”. The feed needs to ask both questions, and each one is then a single quick read.
  • Photos and videos go straight from the phone to blob storage, and a CDN serves them. A post stores only the file’s key.

Post IDs are 64-bit numbers that start with the time (Snowflake-style IDs). So a bigger ID always means a newer post, and sorting by ID is the same as sorting by time. Paging relies on this later.

Pull looks cheap per load. The problem is how often it runs.

Pull or push690,000/spush inserts at peak

Pull reads the recent posts of about 200 followed accounts on every load, so 35 million lookups a second at peak. Push copies each new post into each follower’s timeline ahead of time. That is 100 million posts × 200 followers, or 20 billion copies a day, about 690,000 a second at peak. Opening the feed is then one lookup.

So we push wins: 690,000 small writes are cheaper than 35 million reads.

So push is about 50 times cheaper on average. Remember those two words. Averages hide a problem, and step 3 is about it.

You can change the numbers in the calculator. If people open the feed only once a day, pull becomes cheap and we don’t need fan-out at all. If each person has 1,000 followers, push needs over 2 million copies a second.

Change an assumption
Feed loads, average
58,000/s
Feed loads, peak
170,000/s
Stateless feed servers scale out; the question is what each load costs.
Posts, average
1,200/s
Posts, peak
3,500/s
Pull only: author lists read at peak
35,000,000/s
Tens of millions of lookups a second just to assemble feeds: push to timelines instead.
Push to everyone: inserts at peak
690,000/s
One timeline cache cluster absorbs this; bursts from big accounts are the real limit.
Hybrid: inserts at peak
290,000/s
Skipping inactive followers and pulled accounts removes over half the work.
Timeline cache RAM (30 day actives = 2 × DAU)
15 TB
About 30 bytes an entry in a compact sorted set. Doubled for replicas, this is the biggest cache bill.
Post store (text and metadata, 1 KB a post)
180 TB
Hundreds of TB before replication: shard by post ID from day one.
Media (200 KB a post, averaged)
37 PB
Tens of petabytes: blob storage and a CDN, never the post store.
Feed JSON egress, peak
3.5 GB/s
Twenty 1 KB posts a page. Compressed, under 1 GB/s; images come from the CDN.
Go deeperThe full API2 min

The app needs only two calls. One saves a post, and one reads a page of the feed:

POST /v1/posts Idempotency-Key: 0b7c... { "text": "...", "mediaIds": ["m_9f3a"] } → 201 { "postId" }
GET /v1/feed?limit=20&cursor=... → 200 { "items", "nextCursor" }

The idempotency key is a unique ID the app sends with each post. Phones often send a request again after a timeout. Without the key, a repeated post would be saved twice and copied to every follower twice. With it, the server sees a key it already has, and returns the post it already saved.

Photos are uploaded first, straight to blob storage. The post then refers to them by ID. This keeps large files away from the post service.

POST /v1/media/uploads { "contentType": "image/jpeg", "bytes": 1840221 }
201 { "mediaId": "m_9f3a...", "uploadUrl": "https://blob.../presigned?sig=..." }
POST /v1/posts
Idempotency-Key: 0b7c1e52-...
{ "text": "Sunset at the pier", "mediaIds": ["m_9f3a..."], "audience": "followers" }
201 { "postId": "1849275503923314688" }
GET /v1/feed/new-count?since={postId} { "count": 12 }: asked every 30 to 60 s for the "new posts" button
DELETE /v1/posts/{postId} author only; 204, gone from feeds on the next load
POST /v1/users/{id}/follow 201, or 202 "requested" for a private account
DELETE /v1/users/{id}/follow 204
POST /v1/users/{id}/block 204; also removes follows in both directions

The server answers 400 for invalid content, 429 when someone posts too often, and 409 if a photo is still being processed. A page holds at most 50 posts.

Why does the “new posts” button ask every 30 seconds, instead of the server telling the app? Because telling the app would mean keeping hundreds of millions of connections open. A small count that a cache can answer is far cheaper. A live feed that needs instant updates would use the chat problem’s connection layer instead.

Go deeperThe data model, and which stores to use2 min

First, list how the data is read and written, and how often. The stores follow from this table.

What we do How often What the store must do well
Read my timeline 58,000/s, 170,000/s peak Read one list by user ID
Fetch posts by ID 20 per load, 3.4M/s peak Fetch many posts at once; the popular ones fit in RAM
An author’s recent posts Big accounts, and rebuilds Read one author’s posts, newest first
Followers of X Once per post, by fan-out Read followers 1,000 at a time
Create a post, follow 3,500/s and 1,000/s peak Insert; a follow writes both directions
-- Posts, spread across machines by post_id. The one true copy of each post.
posts (post_id BIGINT PRIMARY KEY, author_id BIGINT, text TEXT,
media LIST<media_ref>, -- keys of files in blob storage, never the files
audience TEXT, -- public | followers | close_friends
created_at TIMESTAMP, edited_at TIMESTAMP,
deleted_at TIMESTAMP) -- the delete mark: any read hides the post at once
-- One author's posts, newest first. Split by month so no single list grows forever.
posts_by_author (author_id BIGINT, month INT, post_id BIGINT,
PRIMARY KEY ((author_id, month), post_id)) WITH CLUSTERING ORDER BY (post_id DESC)
-- Follows, in sharded MySQL. One follow writes both rows.
followers (user_id BIGINT, follower_id BIGINT, PRIMARY KEY (user_id, follower_id))
following (user_id BIGINT, followee_id BIGINT, PRIMARY KEY (user_id, followee_id))
user_counts (user_id BIGINT PRIMARY KEY, followers BIGINT, following BIGINT, mode TEXT) -- push | pull
Timeline cache (Redis, spread across servers by user ID)
timeline:{user_id} sorted set, at most 500 entries, about 30 bytes each
member = post ID and author ID, packed into 16 bytes
score = the time inside the post ID
active:{user_id} when the user was last seen; fan-out skips anyone away for 30 days

Each timeline entry keeps the author’s ID next to the post ID. That looks like a small detail. It lets us hide posts from blocked or unfollowed people without fetching the posts first.

Data Store Why it fits What it costs
Posts Cassandra Handles many writes; easy to spread by ID; runs in regions No transactions; weak lookups by other fields
Posts MySQL Familiar; works when sharded behind a cache, as at Facebook You build and run the sharding yourself
Graph Sharded MySQL + cache A follow is a row; easy to keep exact counts Two writes per follow, fixed up if one fails
Graph Graph database Questions several steps deep A feed asks one step, over billions of follows

Pull does the same work on every read, and reads are 50 times more common than posts. So we move the work to the rarer side. When someone posts, we copy the post’s ID into a short list for each follower. This list is called a timeline. This is called push, or fan-out on write. Three new parts appear: a queue, the workers that do the copying, and a cache that holds the timelines.

News feed · architecture

Stage 2: Push: copy posts ahead of time

UsersCDNimages, videoBlob storemedia originalsAPI gatewayauth, limitsFeed servicereads one timeline57 servers, 60% busyPost service+ publish event3 servers, 57% busyPost storesharded by post21% busyPost cacheposts, author lists57% busyKafkapost eventsnewTimeline cacheuser → post IDsnew3% busyFan-out workerspush to followersnew57 servers, 60% busyGraph servicefollow edges2 servers, 9% busyGraph storeboth directions2% busy
Color keyServiceDatabaseCacheQueue or streamBackground workerNew in this stageAsync or occasional
Stage

Stage 2 of 5

Push: copy posts ahead of time

Why this stage

Feeds are read 50 times more often than posts are written.

Pull repeats the same work on every read, at 35 million lookups a second.

What we add

The post service sends a Kafka event for each post.

Fan-out workers ask the graph service for the author’s followers and add the post ID to each follower’s timeline in a timeline cache.

Opening the feed reads one timeline.

What it costs

About 690,000 timeline inserts a second at peak and 30 TB of memory.

And a post from an account with 80 million followers is 80 million inserts in one burst.

At 170k feed loads per second, every part has headroom. Raise the traffic until something turns red.

Pinch or tap the zoom buttons to read small labels.Drag any box to move it. Reset puts every box back.
The numbers behind the busy bars

How busy it gets

  • For every 50 feed loads, someone publishes 1 new post. Reading is the heavy side.

What one machine can do

  • A feed server builds about 5,000 feeds a second when the timeline is ready. If it has to pull about 200 lists first (stage 1), it manages only about 500.
  • One Redis node answers about 200,000 lookups a second with multi-gets.
  • One fan-out worker copies about 20,000 posts into timelines a second.
  • One ranking server ranks about 500 feeds a second, scoring 500 posts each.

How big the fleet is, per region

  • The post cache needs 30 nodes. Only 2% of reads miss it and go to the post store, which has 16 shards.
  • The timeline cache needs 300 nodes of 100 GB, about 30 TB of memory at about 30 bytes an entry.

Why push costs less than it looks

  • A post is copied to about 84 timelines, not 200. About 40% of followers are inactive and skipped, and 30% of follows point at big accounts, which are pulled instead.

Here is what happens when someone posts:

  1. The post service saves the post on 3 machines. Then it sends a PostCreated event to Kafka, a queue that keeps events in order. Events from the same author stay in order, so a post’s “delete” can never arrive before its “create”. The author gets their reply (201) right away. Nobody waits for the copying.
  2. Fan-out workers read the event. They ask the graph service for the author’s followers, 1,000 at a time.
  3. For each follower, they add the post ID and the author ID to that follower’s timeline. A timeline is a sorted list in Redis, spread across many cache servers. It keeps only the newest 500 entries.

We copy IDs, not whole posts. The post itself is stored only once. So an edit is one write, and a delete is one write. If we copied whole posts, one edit would mean changing millions of copies.

Publishing a post and fanning it out
Timeline cacheGraph serviceFan-out workerKafkaPost storePost serviceAuthor's appTimeline cacheGraph serviceFan-out workerKafkaPost storePost serviceAuthor's apploop[each page of 1,000 followers]POST /posts, Idempotency-Keywrite post at quorumPostCreated(post, author), keyed by author201, nobody waits for fan-outdeliver event (at least once)big account? stop here (step 3)followers page, active in the last 30 dayspipelined ZADD + trim to 500, per shardTimeline cacheGraph serviceFan-out workerKafkaPost storePost serviceAuthor's appTimeline cacheGraph serviceFan-out workerKafkaPost storePost serviceAuthor's apploop[each page of 1,000 followers]POST /posts, Idempotency-Keywrite post at quorumPostCreated(post, author), keyed by author201, nobody waits for fan-outdeliver event (at least once)big account? stop here (step 3)followers page, active in the last 30 dayspipelined ZADD + trim to 500, per shard
Scroll or pinch to zoom, drag to pan
  • Color key
  • Cache
  • Service
  • Background worker
  • Queue or stream
  • Database
FanoutWorker.javafanout_worker.pyFanoutWorker.csfanout_worker.gofanout-worker.ts
// Published by the post service once the post is durable. Kafka keys it by author ID, so one
// author's events stay in order and one consumer handles them.
record PostCreated(long postId, long authorId) {}
record TimelineEntry(long postId, long authorId) {} // the author ID lets readers filter without a lookup
record FollowerPage(List<Long> ids, String next) {} // next is null on the last page
enum AuthorMode { PUSH, PULL }
interface Authors { AuthorMode mode(long authorId); }
interface Graph { FollowerPage followers(long authorId, String cursor, int limit); }
interface Activity { List<Long> activeSince(List<Long> userIds, Instant cutoff); }
interface Timelines {
// One pipelined batch per shard: ZADD timeline:{id} <ms> <postId+authorId, 16 bytes>,
// then ZREMRANGEBYRANK to keep the newest maxLength. The score is the millisecond timestamp,
// not the raw 64 bit ID: Redis scores are doubles and lose precision above 2^53.
void addAll(List<Long> userIds, TimelineEntry entry, int maxLength);
}
// Runs for every PostCreated event. Kafka delivers at least once, so this can run twice for
// the same post; every write it makes must be safe to repeat.
class FanoutWorker {
static final int PAGE_SIZE = 1_000; // followers per batch: one pipelined round trip
static final int TIMELINE_LENGTH = 500; // bounded: memory per user is fixed, about 15 KB
static final Duration ACTIVE_WINDOW = Duration.ofDays(30);
private final Authors authors;
private final Graph graph;
private final Activity activity;
private final Timelines timelines;
private final Clock clock; // injected so tests control "inactive"
FanoutWorker(Authors authors, Graph graph, Activity activity, Timelines timelines, Clock clock) {
this.authors = authors;
this.graph = graph;
this.activity = activity;
this.timelines = timelines;
this.clock = clock;
}
void handle(PostCreated e) {
// Big accounts are never pushed: one post would be tens of millions of inserts and hold
// the fleet for minutes. Followers merge their posts in at read time. The mode is a flag
// set by a background job with hysteresis, so an account near 1M does not flap.
if (authors.mode(e.authorId()) == AuthorMode.PULL) return;
Instant cutoff = clock.instant().minus(ACTIVE_WINDOW);
TimelineEntry entry = new TimelineEntry(e.postId(), e.authorId());
String cursor = null;
do {
// Page through followers so a 900,000 follower account never loads one giant list.
// Production turns each page into its own task so many workers share one big post.
FollowerPage page = graph.followers(e.authorId(), cursor, PAGE_SIZE);
// About 40% of followers have not opened the app in 30 days. Writing to them is
// wasted RAM and work; their timeline is rebuilt by pull if they come back.
List<Long> targets = activity.activeSince(page.ids(), cutoff);
// ZADD is idempotent (same member, same score), so a redelivered event changes nothing.
timelines.addAll(targets, entry, TIMELINE_LENGTH);
cursor = page.next();
} while (cursor != null);
}
}

How much memory do timelines need? Each one is about 15 KB. For a billion people who were active this month, that’s about 15 TB, or 30 TB with a backup copy. That’s roughly 300 cache servers.

Opening the feed is now short. The feed service reads the user’s timeline: up to 500 post IDs, newest first. It fetches the first 20 posts from the post cache and returns them. That’s one list read and one batch of 20 lookups, instead of 200 lookups for pull.

This is enough for a mid-level answer. Pull, why it breaks, and push, with the numbers behind each, explained clearly, passes at mid-level. The next steps are what make it a senior answer. Each one is a place where push breaks at this size.

Go deeperTimeline memory, and a worker that crashes1 min

That 15 KB holds only if Redis stores each timeline in its compact form. By default, Redis switches a sorted set to a bigger form once it has more than 128 entries. That form costs about 3 to 5 times more memory per entry, which would mean 1,000 servers or more. So we raise that limit (zset-max-listpack-entries) to 512. Adding to a compact list of 500 is a little slower, but still takes microseconds. The limit of 500 entries keeps this number under control. Almost nobody scrolls past a few hundred posts. If someone does, we build the older pages with pull.

Interviewer asksA worker crashes halfway through a post with 400,000 followers. What happens?

The worker never marked the event as done in Kafka. So the next worker reads the same event again, and the copying runs again. That’s safe, because adding the same post ID twice still leaves one entry. This property is called being idempotent. Each group of 1,000 followers is its own small task, so a crash repeats only one group, not all 400.

Step 3. A singer with 80 million followers posts

Section titled “Step 3. A singer with 80 million followers posts”
Interviewerminute 20

A singer with 80 million followers posts. Walk me through what your system does. Whether you see the sudden burst, not the average.

On average, push is cheap. But two problems hide behind that average.

The first problem is bursts. All the fan-out workers together copy about 1 million IDs a second. A post to 80 million followers keeps all of them busy for 80 seconds. Every other post waits in line behind it. So the 5-second promise breaks for everyone, several times a day.

The second problem is waste. About 40% of follows point at people who haven’t opened the app in 30 days. Copying posts to them costs work, and their timelines fill memory that nobody reads.

The fix is to use both push and pull, depending on the author. This is called the hybrid.

  • Big accounts are pulled. If an account has more than about 1 million followers, we stop copying its posts. When someone opens their feed, the feed service fetches these accounts’ newest posts and mixes them into the timeline. This is cheap for a simple reason. There are only tens of thousands of such accounts, and millions of feeds read each one. So every feed server keeps their newest post IDs in its own memory, refreshed every second.
  • Inactive people are skipped. Fan-out doesn’t copy posts to anyone who has been away for 30 days. When one of them comes back, the feed service finds no timeline. It builds one with pull, once (about 100 ms), and marks them active again.
News feed · architecture

Stage 3: Push only where it pays

Two kinds of authorUnder 1M followers: pushed intoevery active follower’s timeline.Over 1M: pulled at read time.UsersCDNimages, videoBlob storemedia originalsAPI gatewayauth, limitsFeed service+ merge big accounts71 servers, 60% busyPost service+ publish event3 servers, 57% busyPost storesharded by post21% busyPost cacheposts, author lists60% busyKafkapost eventsTimeline cacheactive users only1% busyFan-out workersskip big, idle24 servers, 60% busyGraph servicefollow edges4 servers, 47% busyGraph storeboth directions2% busy
Color keyServiceDatabaseCacheQueue or streamBackground workerNew in this stageAsync or occasional
Stage

Stage 3 of 5

Push only where it pays

Why this stage

One post from a very big account keeps every fan-out worker busy for over a minute and delays everyone else’s posts.

And 40% of copies go to people who have not opened the app in a month.

What we add

Accounts above about 1 million followers are pulled when the feed is read, and mixed in.

Everyone else is pushed.

Followers who have been away for 30 days are skipped and rebuilt when they return.

What it costs

Two ways to get posts that must agree, a line to tune, and a slower first load for returning users.

At 170k feed loads per second, every part has headroom. Raise the traffic until something turns red.

Pinch or tap the zoom buttons to read small labels.Drag any box to move it. Reset puts every box back.
The numbers behind the busy bars

How busy it gets

  • For every 50 feed loads, someone publishes 1 new post. Reading is the heavy side.

What one machine can do

  • A feed server builds about 5,000 feeds a second when the timeline is ready. If it has to pull about 200 lists first (stage 1), it manages only about 500.
  • One Redis node answers about 200,000 lookups a second with multi-gets.
  • One fan-out worker copies about 20,000 posts into timelines a second.
  • One ranking server ranks about 500 feeds a second, scoring 500 posts each.

How big the fleet is, per region

  • The post cache needs 30 nodes. Only 2% of reads miss it and go to the post store, which has 16 shards.
  • The timeline cache needs 300 nodes of 100 GB, about 30 TB of memory at about 30 bytes an entry.

Why push costs less than it looks

  • A post is copied to about 84 timelines, not 200. About 40% of followers are inactive and skipped, and 30% of follows point at big accounts, which are pulled instead.

Switching an account between push and pull is always safe. When someone opens their feed, we read both kinds of posts, the copied ones and the pulled ones. Then we merge them and remove any duplicates. Here is the code for the newest-first feed. The ranked feed does the same merge, then hands the list to the ranker instead of sorting it by time.

FeedReader.javafeed_reader.pyFeedReader.csfeed_reader.gofeed-reader.ts
record Entry(long postId, long authorId) {}
record Post(long id, long authorId, String text) {}
record Page(List<Post> posts, Long nextBefore) {} // nextBefore is null when the feed ends
interface Timelines { List<Entry> read(long userId, int max); } // pushed by fan-out
// Pulled accounts (held in memory) and the viewer's own recent posts.
interface Pulled { List<Entry> recent(long viewer); }
interface Policy { boolean canSee(long viewer, long authorId); } // follows, blocks, private
interface Posts {
// Only posts that exist, are not deleted and whose audience includes the viewer come back.
Map<Long, Post> getVisible(List<Long> ids, long viewer);
}
// One page of the chronological feed: the pushed timeline merged with pulled posts, filtered
// at read time, and cut at a cursor that new posts cannot shift.
class FeedReader {
private final Timelines timelines;
private final Pulled pulled;
private final Policy policy;
private final Posts posts;
FeedReader(Timelines timelines, Pulled pulled, Policy policy, Posts posts) {
this.timelines = timelines;
this.pulled = pulled;
this.policy = policy;
this.posts = posts;
}
Page read(long viewer, Long before, int limit) {
// Candidates from both sides of the hybrid: what fan-out wrote, and the recent posts
// of followed accounts that are never fanned out, plus my own (read your own write).
List<Entry> candidates = new ArrayList<>(timelines.read(viewer, 500));
candidates.addAll(pulled.recent(viewer));
// Post IDs are time-sortable and unique, so sorting by ID is time order with no ties.
candidates.sort(Comparator.comparingLong(Entry::postId).reversed());
List<Post> page = new ArrayList<>(limit);
Set<Long> seen = new HashSet<>();
int i = 0;
while (page.size() < limit && i < candidates.size()) {
List<Long> batch = new ArrayList<>();
// Over-fetch: some candidates will be filtered, and one extra round trip is
// cheaper than a short page.
while (batch.size() < limit * 2 && i < candidates.size()) {
Entry c = candidates.get(i++);
// The cursor means "older than this post", not "skip N". Posts that arrive at
// the top while the user scrolls cannot push items into the next page twice.
if (before != null && c.postId() >= before) continue;
// An author who just switched between push and pull can be in both lists.
if (!seen.add(c.postId())) continue;
// Unfollows and blocks apply here, so they work on the very next load even
// though fan-out already wrote the entry. Cleaning the cache can be lazy.
if (!policy.canSee(viewer, c.authorId())) continue;
batch.add(c.postId());
}
// Hydration is where deletes and audience changes are enforced: the post store is
// the source of truth, the timeline only holds IDs.
Map<Long, Post> found = posts.getVisible(batch, viewer);
for (long id : batch) {
Post p = found.get(id);
if (p != null && page.size() < limit) page.add(p);
}
}
// The next page starts below the last post shown. The client treats it as opaque.
Long next = page.size() < limit ? null : page.get(page.size() - 1).id();
return new Page(page, next);
}
}

The hybrid has a price. There are now two ways to get posts, and they must agree on order, filtering and paging. There’s a line to tune. And someone who comes back after a month waits about 100 ms longer for their first load.

Go deeperWhere exactly to draw the line2 min

First, here is how the three choices compare at peak:

Strategy Copies at peak One feed load reads A big account’s post appears
Pure push 690k/s + bursts 1 list After a minute, and delays others
Pure pull 0 ~200 lists At once
Hybrid: pull above 1M, skip idle 290k/s 1 list + a few dozen in-memory lists At once

Where does 290k/s come from? Start with pure push: 690,000 copies a second at peak. Skipping inactive people removes the 40% of follows that point at them, which leaves about 414,000. Then assume accounts above 1 million followers make up about 30% of what’s left. That is an estimate, not a measured number: there are few such accounts, but each of their posts goes to so many people that they add up fast. Pulling them instead removes those copies too: 414,000 × 0.7 is about 290,000 a second.

We said “about 1 million followers”. But think about an account sitting right at 1 million. As people follow and unfollow, it would switch between push and pull every day. Each switch is safe, but it is wasted work, and it makes the system harder to reason about.

So we use two lines instead of one. An account moves to pull when it passes 1 million followers. It only moves back to push when it drops below 800,000. The gap between the two lines stops the flipping. Engineers call this gap hysteresis.

Why 1 million, and not 100,000 or 10 million? Two things set it:

  • Burst size. At 1 million followers, a post takes the whole fan-out fleet about one second. That still fits inside the 5-second promise, even with a few such posts at once. At 10 million, it doesn’t.
  • Memory on each feed server. Every pulled account’s newest post IDs sit in memory on every feed server. Tens of thousands of accounts × a few hundred IDs is a few hundred MB. That fits easily. If the line were 10,000 followers, there would be millions of pulled accounts, and that no longer fits.

Count active followers, not all followers. Bot farms make raw follower counts much larger than the real audience.

Product usually wants the best posts first, not the newest. So ranking needs a clear place in the design, a time limit, and a plan for when it is slow.

News feed · architecture

Stage 4: The best posts first

Two kinds of authorUnder 1M followers: pushed intoevery active follower’s timeline.Over 1M: pulled at read time.UsersCDNimages, videoBlob storemedia originalsAPI gatewayauth, limitsFeed servicecandidates → ranker71 servers, 60% busyPost service+ publish event3 servers, 57% busyPost storesharded by post21% busyRanking service+ feature cachenew567 servers, 60% busyPost cacheposts, author lists60% busyKafkapost eventsTimeline cacheactive users only1% busyFan-out workersskip big, idle24 servers, 60% busyGraph servicefollow edges4 servers, 47% busyGraph storeboth directions2% busy
Color keyServiceDatabaseCacheQueue or streamBackground workerNew in this stageAsync or occasional
Stage

Stage 4 of 5

The best posts first

Why this stage

Product wants a ranked feed, not newest first, and the feed must still load in 300 ms.

What we add

A ranking service scores about 500 candidates for each load, with a 100 ms time limit.

If it is slow, the feed falls back to time order.

What it costs

Ranking servers are the largest compute cost, and paging now needs a saved session, because the order is no longer by time.

At 170k feed loads per second, every part has headroom. Raise the traffic until something turns red.

Pinch or tap the zoom buttons to read small labels.Drag any box to move it. Reset puts every box back.
The numbers behind the busy bars

How busy it gets

  • For every 50 feed loads, someone publishes 1 new post. Reading is the heavy side.

What one machine can do

  • A feed server builds about 5,000 feeds a second when the timeline is ready. If it has to pull about 200 lists first (stage 1), it manages only about 500.
  • One Redis node answers about 200,000 lookups a second with multi-gets.
  • One fan-out worker copies about 20,000 posts into timelines a second.
  • One ranking server ranks about 500 feeds a second, scoring 500 posts each.

How big the fleet is, per region

  • The post cache needs 30 nodes. Only 2% of reads miss it and go to the post store, which has 16 shards.
  • The timeline cache needs 300 nodes of 100 GB, about 30 TB of memory at about 30 bytes an entry.

Why push costs less than it looks

  • A post is copied to about 84 timelines, not 200. About 40% of followers are inactive and skipped, and 30% of follows point at big accounts, which are pulled instead.

Timelines keep posts in time order. Ranking doesn’t replace them. It uses them as the starting point. The 500 newest IDs in a timeline, plus the posts from big accounts, become the candidates. The ranker scores them and returns the best ones.

Why not score each post once, when it is copied into timelines? Because the most useful signals don’t exist yet at that moment. How many likes a post gets in its first ten minutes matters a lot. So does what this person did a minute ago. Also, if scores were stored in timelines, every new model would mean rewriting every timeline. So timelines stay in time order, and ranking happens when the feed is read.

Ranking is not free. It adds about 45 ms to each load, uses most of the feed’s CPU, and needs several hundred servers at peak. So the feed service gives the ranker 100 ms. If the ranker takes longer, the feed shows the last ranked page from the past 10 minutes, or else the posts in time order. If the ranker goes down, the feed gets worse, but it never goes empty.

Now follow one feed load through the design as it stands. Next hop moves the request one step. The bar shows how much of the 300 ms budget each step uses.

News feed · architecture

Trace a request: Load the home feed

Two kinds of authorUnder 1M followers: pushed intoevery active follower’s timeline.Over 1M: pulled at read time.UsersCDNimages, videoBlob storemedia originalsAPI gatewayauth, limitsFeed servicecandidates → rankerPost service+ publish eventPost storesharded by postRanking service+ feature cachePost cacheposts, author listsKafkapost eventsTimeline cacheactive users onlyFan-out workersskip big, idleGraph servicefollow edgesGraph storeboth directions
Color keyServiceDatabaseCacheQueue or streamBackground workerNew in this stageAsync or occasionalHop in motionHops doneResponse

Load the home feed: step 1 of 8

Users to API gateway

GET /v1/feed?limit=20. The gateway terminates TLS, checks the session token and the per-user rate limit, and routes to any feed server in the user’s home region.

20 ms of a 300 ms budget
  1. Users → API gateway20 ms
  2. API gateway → Feed service2 ms
  3. Feed service → Timeline cache3 ms
  4. Feed service → Graph service4 ms
  5. Feed service → Post cache3 ms
  6. Feed service → Ranking service45 ms
  7. Feed service → Post cache6 ms
  8. Response
Pinch or tap the zoom buttons to read small labels.Drag any box to move it. Reset puts every box back.
Go deeperInside the ranker: two passes and a feature cache2 min

Scoring 550 posts with a large model would take too long. So the ranker works in two passes.

  1. A fast first pass. A small, simple model looks at every candidate and keeps about 100. It uses only cheap signals, like the post’s age and how often this viewer interacts with the author.
  2. A careful second pass. A larger model puts those 100 in order. It uses many more signals: how well the post is doing right now, what kind of posts this viewer likes, and whether it shows a photo or video.

All of these signals are called features. Background jobs work them out ahead of time and keep them in a feature cache. So when someone opens the feed, the ranker only reads numbers. It never calculates anything slow while the user waits.

Last, a few simple rules run after scoring. For example, don’t show three posts in a row from the same author. These rules are cheap and easy to change without training a new model.

The design is complete. Now we test it against four situations every feed must handle: scrolling while new posts arrive, deletes and blocks, a returning user with no timeline, and ten times the users. Only the last one adds boxes.

You are scrolling, and new posts keep arriving. You should never see a post twice, and never miss one.

The simplest idea is page numbers: page 1, page 2, page 3. It breaks quickly. Say ten new posts arrive while you read page 1. Everything moves down by ten, so page 2 now starts with ten posts you’ve already seen.

Instead, each page comes with a cursor: a bookmark that says where you stopped. What the bookmark holds depends on the feed.

In the newest-first feed, the cursor is the ID of the last post you saw. The next page is “posts older than this one”. New posts get bigger IDs, so they appear above your bookmark and never move it. (Two ID servers’ clocks can differ by a few milliseconds, so the order is only exact to within a few milliseconds. That’s fine for a feed.)

In the ranked feed, “older than this one” doesn’t work, because the order isn’t by time. So we rank once. On the first page, we rank about 500 posts and save the ordered list of IDs for 30 minutes. We call this saved list a session. The cursor holds the session and your place in it. Each next page reads the next 20 IDs from the session, without ranking again. New posts don’t enter the session. Instead, a “12 new posts” button appears. Pulling down to refresh starts a new session.

There are two small side effects. If a post is deleted during your session, that page shows 19 posts instead of 20. And if the session expires, we rank again. Almost nobody notices either.

Go deeperWhat the cursor looks like1 min

To the app, the cursor is just a string, like eyJzIjoiZjQ.... It must not try to read it. Inside, the server packs a few fields and signs them, so nobody can edit the cursor to see other people’s posts:

newest first: { "before": 1849275503923314688 }
ranked: { "session": "f4c1...", "offset": 40, "expires": 1792000000 }

Because the cursor carries everything, any feed server can answer the next page. No server needs to remember who is scrolling where. The ranked session itself sits in the cache, about 4 KB for 500 IDs. If the session has expired, the server quietly starts a new one from the top.

A user deletes a post that went to 300,000 timelines, or blocks someone. How fast does it disappear?

Remember that we copy post IDs into timelines. A deleted post’s ID now sits in 300,000 timelines, plus some saved sessions. We can’t remove every copy before each of those people opens the app again. So removing copies can’t be the promise. Instead, we check every post at the moment we read it.

  • Deletes. When a post is deleted, the post service marks it as deleted. This mark is called a tombstone. It also puts the tombstone in the post cache. Every feed load fetches its 20 posts from that cache. So on the next load, everywhere, the deleted post is skipped. This covers saved sessions too.
  • Privacy changes work the same way. Every time we fetch a post, we check that this viewer is still allowed to see it.
  • Unfollows and blocks. Each timeline entry stores the author’s ID next to the post ID. So when we merge, we drop posts from anyone the viewer no longer follows or has blocked.

We still remove the old copies, but only as cleanup. The delete sends a PostDeleted event through Kafka, in the same order as the original post. Fan-out workers then remove the ID from timelines, so pages don’t come up short.

A delete is enforced on read, then cleaned up
Feed serviceFan-out workerKafkaPost cachePost serviceAuthorFeed serviceFan-out workerKafkaPost cachePost serviceAuthorDELETE /posts/123tombstone in the post storereplace post 123 with a tombstonePostDeleted(123), keyed by author204fetch 123 for any viewertombstone: skip it on this very loadPostDeleted(123)ZREM 123 from active followers' timelinesFeed serviceFan-out workerKafkaPost cachePost serviceAuthorFeed serviceFan-out workerKafkaPost cachePost serviceAuthorDELETE /posts/123tombstone in the post storereplace post 123 with a tombstonePostDeleted(123), keyed by author204fetch 123 for any viewertombstone: skip it on this very loadPostDeleted(123)ZREM 123 from active followers' timelines
Scroll or pinch to zoom, drag to pan
  • Color key
  • Service
  • Background worker
  • Queue or stream
  • Cache

This also answers “is the feed consistent?”. It depends on the action. If a friend’s post shows up one second late, nobody notices. So new posts can arrive a little late. This is called eventual consistency. But if a blocked person’s post shows up “for a few minutes”, that’s a safety problem. So blocks and deletes are checked on every read.

Go deeperWhat each action promises1 min

Each action gets its own promise. Strong promises cost more, so we spend them only where users would notice.

Action Promise What users can see
Post Saved on 3 machines before the reply A post that got “saved” is never lost
Author sees own post At once The merge always includes the author’s own posts
Followers see a post Usually 1 s, at most 5 s Two friends side by side may see it a second apart
Order while scrolling One ranked session Posts don’t move while you scroll; refresh reorders
Delete, block, unfollow, privacy From the next load Gone everywhere on the next load; cleanup comes later
Follow Within seconds The new account’s last 20 posts appear after a moment
Another region 1 to 2 s later A follower abroad sees a post a little later

Someone comes back after two months away. Fan-out stopped copying posts to them after 30 days, so they have no timeline in the cache. Their first load must still be quick, and never empty.

The feed service sees the missing timeline and builds it with pull, the step 1 path: it reads the newest posts of the 200 or so accounts they follow, merges them, and saves the result as their timeline. That costs a few hundred milliseconds, once. From then on, fan-out copies new posts to them again. If many people come back at once, after an outage or a big event, rebuilds are limited per second, and the rest see a time-order feed until their turn.

Ten times the users means about 580,000 feed loads a second on average, and 1.7 million at peak. Most parts simply grow: timelines are split by user, posts by post ID, and the fan-out workers by Kafka partition. Two parts feel it first. The post cache must answer about 36 million fetches a second, so the posts of big accounts are also kept in each feed server’s own memory for a few seconds. And the ranker grows toward thousands of servers, which makes its time limit and fallback matter more, not less.

The users are also spread over the world, a third in each of three regions. Losing a whole region is the other risk. At 99.99%, a one-hour outage uses up more than a year’s allowed downtime. So each region can serve feeds and copy posts on its own. Each person has a home region that holds their timeline, so we don’t keep three copies of every timeline. If a region fails, its users move to another region, and their feeds are rebuilt with pull, a few at a time.

News feed · architecture

Stage 5: Regions

Every region runs this stackEach user’s timeline lives intheir home region. Posts andpost events replicate to all.UsersCDNimages, videoBlob storemedia originalsAPI gatewayauth, limitsFeed servicecandidates → ranker71 servers, 60% busyPost service+ publish event3 servers, 57% busyPost storesharded by post7% busyRanking service+ feature cache567 servers, 60% busyPost cacheone per region20% busyKafkamirrored per regionTimeline cachein home region1% busyFan-out workersskip big, idle24 servers, 60% busyGraph servicefollow edges4 servers, 47% busyGraph storeboth directions2% busy
Color keyServiceDatabaseCacheQueue or streamBackground workerNew in this stageAsync or occasional
Stage

Stage 5 of 5

Regions

Why this stage

One region is one failure away from an outage, against a 99.99% target.

Users in Asia also pay 150 ms or more to reach a US region.

What we add

Every region runs the whole stack.

Each user has a home region that holds their timeline.

Posts and post events are copied to every region.

What it costs

A new post appears a second or two later abroad, and when a region fails, the moved users’ timelines are rebuilt.

At 170k feed loads per second, every part has headroom. Raise the traffic until something turns red.

Pinch or tap the zoom buttons to read small labels.Drag any box to move it. Reset puts every box back.
The numbers behind the busy bars

How busy it gets

  • For every 50 feed loads, someone publishes 1 new post. Reading is the heavy side.

What one machine can do

  • A feed server builds about 5,000 feeds a second when the timeline is ready. If it has to pull about 200 lists first (stage 1), it manages only about 500.
  • One Redis node answers about 200,000 lookups a second with multi-gets.
  • One fan-out worker copies about 20,000 posts into timelines a second.
  • One ranking server ranks about 500 feeds a second, scoring 500 posts each.

How big the fleet is, per region

  • The post cache needs 30 nodes. Only 2% of reads miss it and go to the post store, which has 16 shards.
  • The timeline cache needs 300 nodes of 100 GB, about 30 TB of memory at about 30 bytes an entry.

Why push costs less than it looks

  • A post is copied to about 84 timelines, not 200. About 40% of followers are inactive and skipped, and 30% of follows point at big accounts, which are pulled instead.
Interviewerminute 39

Your timeline cache loses a shard. What do users see?

What they are testing

Whether you know the timeline is only a cache that can be rebuilt.

Our first promise was that the feed always loads. It may be a little old, not ranked, or slower, but it is never empty. Three choices we already made keep that promise:

  • Timelines are only a cache. If a cache server is lost, those users’ next load is built with pull, the same way we build feeds for people coming back after a month. It’s slower, but it works.
  • The ranker has a time limit. If it’s slow or down, the feed falls back to time order.
  • Posts are saved before they’re copied. If Kafka or the workers get stuck, posts arrive late, but none are lost.

Click any box to break it and see what users would notice:

News feed · architecture

Break it: what fails, who notices, what users see

Two kinds of authorUnder 1M followers: pushed intoevery active follower’s timeline.Over 1M: pulled at read time.UsersCDNimages, videoBlob storemedia originalsAPI gatewayauth, limitsFeed servicecandidates → rankerPost service+ publish eventPost storesharded by postRanking service+ feature cachePost cacheposts, author listsKafkapost eventsTimeline cacheactive users onlyFan-out workersskip big, idleGraph servicefollow edgesGraph storeboth directions
Color keyServiceDatabaseCacheQueue or streamBackground workerNew in this stageAsync or occasional
Break

Failure: click any box to break it

A timeline cache shard goes down

What breaks

Timelines on that shard are unreadable until its replica takes over; if both are lost, they are gone.

How we notice

Timeline miss rate per shard.

What users see

Affected users get a feed rebuilt by pull: slower, otherwise the same.

What we do

Timelines are a cache, not a source of truth.

The rebuild path recreates them on demand, rate-limited.

Pinch or tap the zoom buttons to read small labels.Drag any box to move it. Reset puts every box back.

When the system is overloaded, we turn things off in a fixed order. First the second ranking pass, then all ranking, then the posts from big accounts. Reading the timeline and fetching posts are the last to go.

Go deeperEvery failure, and what users see2 min
Part Failure What users see How we notice What we do
Timeline cache shard Lost Next load built by pull, a little slower Miss rate per shard Backup copy takes over; rebuilds are limited per second
Fan-out workers Down or slow New posts arrive late; nothing is lost How far behind Kafka they are Add workers on lag; repeated inserts are harmless
Kafka Down Copied posts stop arriving; authors see their own Errors when sending Posts are saved first; a repair job sends them again
Ranker Slow or down Feed in time order instead of ranked Fallback rate, ranker p99 100 ms limit; last ranked page, or time order
Post cache node Down Slower loads for a few seconds Hit rate, store reads Backup copy takes over; big accounts also in server memory
Post store shard Too few copies Some posts fail to save; a few old posts skipped Write errors per shard 3 copies; fetching skips a missing post, never fails the page
Graph service Down Copying stalls; big accounts’ posts missing Graph error rate Last cached lists; Kafka retries the copying
Whole region Down Users move in 1 to 2 minutes; feeds rebuilt by pull Test requests from outside Rebuilds limited per second; time-order feed meanwhile

Where should timelines live when there are three regions?

Where timelines live Memory When a region fails
Home region only 30 TB Rebuilt by pull, a few at a time
Plus a partner region 60 TB A warm copy is ready; every insert goes twice
Every region 90 TB Nothing to do; but copying runs 3 times

We pick the home region only. Losing a whole region is rare, and rebuilding is slower but works. Paying for three times the memory every day, for a rare event, isn’t worth it.

Go deeperRunning it: alerts, abuse and cost2 min

Wake someone up when: fewer than 99.95% of feed loads work in a region for 5 minutes; the slowest 1% of loads take over 300 ms for 10 minutes; the oldest waiting post event is more than 30 seconds old; the ranker falls back more than 2% of the time; or timeline misses jump, which means a lost shard or a wave of rebuilds.

Abuse. A feed spreads whatever gets into it. So we limit how fast each account and device can post and follow. Posts flagged as spam when they are written are never copied. Posts flagged later get a tombstone, and the read-time check hides them everywhere.

Cost, largest first: sending photos and video through the CDN, ranking servers, timeline memory, then media storage. At 10 times the users, the post cache feels it first, at 36 million fetches a second, and the ranker grows toward thousands of servers.

Here is everything we built, in the order we built it: pull, then push, then the hybrid for big accounts, then ranking, then regions. Use the buttons to step through it.

News feed · architecture

The full design, stage by stage

Every region runs this stackEach user’s timeline lives intheir home region. Posts andpost events replicate to all.UsersCDNimages, videoBlob storemedia originalsAPI gatewayauth, limitsFeed servicecandidates → ranker71 servers, 60% busyPost service+ publish event3 servers, 57% busyPost storesharded by post7% busyRanking service+ feature cache567 servers, 60% busyPost cacheone per region20% busyKafkamirrored per regionTimeline cachein home region1% busyFan-out workersskip big, idle24 servers, 60% busyGraph servicefollow edges4 servers, 47% busyGraph storeboth directions2% busy
Color keyServiceDatabaseCacheQueue or streamBackground workerNew in this stageAsync or occasional
Stage

Stage 5 of 5

Regions

Why this stage

One region is one failure away from an outage, against a 99.99% target.

Users in Asia also pay 150 ms or more to reach a US region.

What we add

Every region runs the whole stack.

Each user has a home region that holds their timeline.

Posts and post events are copied to every region.

What it costs

A new post appears a second or two later abroad, and when a region fails, the moved users’ timelines are rebuilt.

At 170k feed loads per second, every part has headroom. Raise the traffic until something turns red.

Pinch or tap the zoom buttons to read small labels.Drag any box to move it. Reset puts every box back.The whole design. Step through it: pull, push, the hybrid, ranking, then regions.
The numbers behind the busy bars

How busy it gets

  • For every 50 feed loads, someone publishes 1 new post. Reading is the heavy side.

What one machine can do

  • A feed server builds about 5,000 feeds a second when the timeline is ready. If it has to pull about 200 lists first (stage 1), it manages only about 500.
  • One Redis node answers about 200,000 lookups a second with multi-gets.
  • One fan-out worker copies about 20,000 posts into timelines a second.
  • One ranking server ranks about 500 feeds a second, scoring 500 posts each.

How big the fleet is, per region

  • The post cache needs 30 nodes. Only 2% of reads miss it and go to the post store, which has 16 shards.
  • The timeline cache needs 300 nodes of 100 GB, about 30 TB of memory at about 30 bytes an entry.

Why push costs less than it looks

  • A post is copied to about 84 timelines, not 200. About 40% of followers are inactive and skipped, and 30% of follows point at big accounts, which are pulled instead.
Decision We chose We gave up We’d choose differently if
Fan-out Push under ~1M followers, pull above One simple read path No big accounts (push), or few reads per user (pull)
Feed storage 500 IDs per user in Redis Unlimited saved history Users often scroll thousands of posts deep
Inactive users No copying after 30 days, rebuild on return An instant first load after a break Memory were cheap compared with rebuilds
Ranking When the feed is read, two passes, 100 ms limit Cheap reads The product were newest-first only
Paging ID cursor (newest first), session (ranked) New posts appearing without refresh A live event feed (push over an open connection)
Deletes, blocks Checked on every read, cleaned up later A simpler read path Never: the other way shows deleted posts
  • 0 to 6. The four scope questions. Ask about the biggest account directly.
  • 6 to 9. Reads and posts, as operations per second.
  • 9 to 20. Pull first, then the number that breaks it, then push: writing a post and reading the feed. A mid-level answer can stop here.
  • 20 to 26. Big accounts: the burst, the hybrid, skipping inactive users.
  • 26 to 39. Ranking with a time limit, then the stress tests: paging, deletes, a returning user and ten times the users. This is where senior answers win or lose.
  • 39 to 45. Losing the cache, then your own questions.

Never skip the push and pull math, the hybrid, or checking deletes on read. If time is short, drop regions and database brand names.

Interviewer asksProduct wants posts from accounts you don't follow, like recommended reels.

That’s just another source of candidates. A recommendation service adds a few hundred posts per person, and the ranker scores them along with posts from people you follow. Fan-out doesn’t change. This is why we kept gathering posts and ranking them as separate steps.

Interviewer asksHow would this change at 10 times the users?

Feed servers, rankers and caches can simply grow. The pressure moves to three places. Fetching posts: keep the hottest posts in memory on every feed server. The ranker’s cost: fewer candidates and a smaller first pass. And timeline memory, which grows with users: shorter timelines, or a shorter “active” window. The hybrid design stays the same.

Go deeperMore follow-up questions2 min

A brand new user follows nobody. What do they see? There’s nothing to read yet. So sign-up suggests accounts to follow. Until they follow enough, the feed shows popular posts from their region, labelled as suggestions. Each new follow also copies that account’s last 20 posts into their timeline. Otherwise, following someone would show nothing until that person posts again.

A private account approves a new follower. When do they see old posts? Approval creates the follow and copies recent posts, the same as any follow. And because who can see what is checked on every read, nothing appears before approval, and everything appears right after, even if the copying is slow.