LearnHLDDesign a news feed

Design a news feed

Reading a feed has to be instant. Nobody waits for a feed. Writing a post can take a minute and nobody would notice, because the person who wrote it already knows what it says.

That asymmetry is the entire design. Everything below is a way of moving work off the read and onto the write, and then dealing with the one case where that stops being possible.

Your answer

You precompute each user's feed when their friends post. A celebrity with 40 million followers posts. What exactly goes wrong, and would adding servers fix it?

What you are building

In scope
  • Post something. Text and a media reference. The media pipeline is somebody else’s design question.
  • Follow and unfollow. Asymmetric, so a follow is not a friendship.
  • Read your feed, newest first, paginated. This is 99% of the traffic and all of the latency budget.
The numbers you commit to
  • Feed loads in under 200ms at p95.
  • 300 million daily active users, opening the feed about 10 times a day.
  • A post shows up for followers within a few seconds, not instantly.
  • The feed can be slightly stale. It cannot be empty or out of order.
Cut, and say so out loud
  • Ranking and personalisation. Enormous, and it sits on top of this rather than inside it. Say you would keep the retrieval layer separate from the ranking layer.
  • Media upload, transcoding and delivery.
  • Notifications, which look similar and are a separate fan out problem.
  • Ads insertion, which is a merge step at read time.

The numbers

Fan out cost
300 million
0.5
200
Posts per second300M x 0.5 / 86,400 = 1,736
Feed opens per second300M x 10 / 86,400 = 34,722
Feed rows written per second1,736 x 200 = 347,222
Amplification200x on every post
347,222 writes/sec
the real cost of precomputing feeds

Reads are 34,722 a second and would be a simple lookup. The fan out turns one post into 200 writes, so the write path is now 10.0 times the read path. That is the trade: you are buying a fast read with a very expensive write, and it works until one user has far more followers than the average.

The API

GET/v1/feed?cursor=eyJ0IjoxNzU2...&limit=20
returns 200 { items: [...], nextCursor: "..." }
Why: Cursor pagination, not offset. A feed shifts constantly as new posts arrive, so page 2 by offset shows you items you already saw on page 1. The cursor encodes a position in the ordering rather than a count.
POST/v1/posts
{
  "text": "...",
  "mediaId": "m_88123",
  "clientId": "c_4f21ab"
}
returns 202 { "postId": "...", "state": "FANNING_OUT" }
Why: 202, because the post is stored but has not reached anyone yet. The clientId is an idempotency key so a retry on a flaky connection does not post twice, which is a mistake users notice immediately.
POST/v1/follows
{ "targetId": 88231 }
returns 204
Why: A follow is asymmetric and it changes what you will see going forward, not retroactively. Deciding whether a new follow backfills their old posts into your feed is a product decision worth asking about out loud.

The data model

postssharded by post_id, time ordered ids
post_idbigintPKSnowflake style, so it sorts by time without a secondary index.
author_idbigintIDXFor the profile page, which is a different query from the feed.
textvarchar(2048)
media_idvarchar(64)A reference. Never the bytes.
created_attimestamp
Sample row
1927334455667788 | 88231 | "shipped it" | m_88123 | 2026-08-25 14:32
One row per post, written once, read constantly. This table is small and it is the source of truth. Everything else is derived and can be rebuilt.
feedRedis list or a wide column store, sharded by user_id
user_idbigintPKThe reader, not the author. This is the whole point.
post_idslist<bigint>Newest first, capped at about 800 entries. Ids only, never post content.
Sample row
4471 | [1927334455667788, 1927334455661234, ...]
Ids only. Storing post content per follower would multiply every post by its follower count in storage as well as in writes, and it would make editing a post impossible. Reading a feed is one list read plus one batch fetch of posts by id, which is two round trips and both are cached.

The cap matters. Nobody scrolls past a few hundred items, so trimming the list keeps storage bounded and makes the write cheap. If someone does scroll to the end, fall back to generating older pages on demand, which is rare enough to be slow.

The design

Figure 1. Posting is asynchronous and expensive. Reading is a list lookup and a batch fetch, and both hit cache.

The queue between the post service and the fan out workers is doing more than decoupling. It absorbs the burst when a popular account posts, it lets fan out be retried independently of the post succeeding, and it means the user’s post is durable before any of the expensive work starts.

Deep dive one: push, pull, or both

Feed strategy
Precompute every follower’s feed at post time. Reads become a single list lookup, which is as fast as this gets. Writes cost one operation per follower, so the whole model rests on the assumption that follower counts are modest. For the vast majority of users they are.

Deep dive two: why the celebrity breaks it

The fan out model assumes follower counts cluster around an average. They do not. They follow a power law, and a power law has no useful average.

Distribution
82
12
4.0
1.2
0.5
0.2
0.1
0.0
0
1
2
3
4
5
6
7
Bucket 0 carries 82.0% (even would be 12.5%)
Grouped by follower count, each bucket ten times the last. The overwhelming majority of accounts have very few followers, which is exactly why fan out on write looks so reasonable when you size it from the average.

Switch between those two and the trap is obvious. Sizing the system from the first chart gives you a design that the second chart destroys.

Concretely: an account with 40 million followers posts once. That is 40 million list writes from one HTTP request. Adding fan out workers does not fix it, because the work is real, it is not parallelism you are short of, and the last follower gets the post minutes after the first. Meanwhile the fan out queue is full of one person’s post and everybody else’s posts are stuck behind it.

The fix has two halves. Do not fan out above a threshold, and merge those authors in at read time instead. And give large accounts their own queue partition so that when you do fan out to a large but not celebrity account, it does not block everyone else.

The follow up you will get

“Where do you set the threshold?” Not from first principles. It is where fan out cost crosses read merge cost, and that depends on your read to write ratio and how many celebrities a typical user follows. Start somewhere defensible, around ten to a hundred thousand followers, instrument both paths, and move it. Saying you would measure it is a better answer than a confident number, because a confident number here is invented.

Deep dive three: keeping the feed correct

Precomputed feeds are derived data, and derived data drifts.

Unfollow. Their posts are already sitting in your list. Filtering at read time is the usual answer, since scrubbing the list is expensive and unfollows are rare. Filter on hydration, when you have the author ids anyway.

Deleted posts. Same shape. The id stays in feed lists and hydration returns nothing, so the read path has to tolerate gaps and fetch a few extra to fill the page.

A new follow. The feed has none of that author’s history. Either backfill their recent posts into the list, or accept that the feed fills in going forward. Backfilling is nicer and it is a product decision worth naming.

Rebuilding. Feed lists are a cache with an expensive miss. You need a job that can rebuild one user’s feed from posts and follows, both for corruption and for the day somebody changes the retention rules. If you cannot rebuild it, the derived data is really a source of truth, and now you have two.

Break it

Fan out load
typical
typicalpopular postcelebrity postshybrid
Healthy. Average author, 200 followers. About 350,000 feed writes a second across the fleet, spread evenly. Queue depth is near zero and posts land in followers’ feeds in under a second.

Trade-offs

ChoiceWhat you gainWhat you payPick it when
Fan out on writeA feed read is one list lookup, which is the fastest this can be and scales with cheap read capacity.One post becomes N writes, and work is wasted on users who never open the app.The default for the vast majority of accounts, where N is small.
Fan out on readNothing stored, nothing to rebuild, and no wasted work. Always current.Every read scatters across hundreds of authors, which is where your latency budget dies.Celebrity accounts, and users who are rarely active enough to justify precomputation.
Storing post ids in feedsBounded storage, and editing or deleting a post takes effect everywhere at once.A second round trip to hydrate, and the read path has to tolerate ids that no longer resolve.Almost always. Storing content per follower multiplies storage by follower count.
Capping feeds at a few hundred entriesStorage and write cost stay bounded no matter how long an account exists.Deep scrolling falls back to a slower on-demand path.Always. The number of users who scroll past a few hundred items is small enough to serve slowly.

Interview replay

Interviewer
How would you build the feed read path?
Open, and the first choice you name tells them how far you have thought.
You
I would precompute it. Reads are about ten times writes here and nobody tolerates a slow feed, so I want the read to be a single list lookup of post ids, then a batch hydrate of those posts. That means doing the work at write time, when the user is not waiting.
Leads with the asymmetry between reads and writes, which is the reason for everything else.
Interviewer
Someone with 40 million followers posts. What happens?
The real question. Everything before was warm up.
You
One request becomes 40 million list writes. The fan out queue fills with that single post’s work, everyone else’s posts are stuck behind it, and the last follower gets it minutes after the first. Adding workers does not fix it, because the work is genuinely 40 million writes. So above a follower threshold I stop fanning out entirely and merge those authors at read time instead. A reader follows very few celebrities, so the merge is a small bounded cost.
Explains why the obvious fix fails before giving the real one. That ordering is what makes it sound like experience rather than recall.
Interviewer
A user unfollows someone. Their posts are still in the precomputed feed.
Testing whether you have thought about derived data going stale.
You
Filter at read time. I already have the author ids when I hydrate, so checking them against the follow list is cheap, and unfollows are rare enough that scrubbing every affected feed list is not worth it. Deleted posts work the same way: the id stays, hydration returns nothing, and the read path fetches a few extra to fill the page.
One mechanism covering two problems, and the reason it is cheap.
Interviewer
Where would ranking fit?
Checking whether you would bolt it into the wrong layer.
You
On top, as a separate stage. Retrieval produces a few hundred candidates, which is what we just built, and ranking scores and reorders them at read time. Keeping them separate means the ranking model can change weekly without touching the storage layer, and it means a ranking outage degrades to reverse chronological rather than to an empty feed.
The failure mode of the ranking layer is the detail that shows this is a real boundary and not just a diagram.

Checkpoint

Checkpoint

1. Why does adding more fan out workers not solve the celebrity problem?

2. Why store post ids in each feed rather than the post content?

3. A user follows 5,000 accounts and the feed read is now slow. What is the most likely cause?

Say this in 60 seconds

Reads dominate and nobody waits for a feed, so I would move the work to write time: when someone posts, fan out the post id into a precomputed list for each follower, capped at a few hundred entries. A feed read is then one list lookup plus a batch hydrate of posts by id, and both are cacheable. That model breaks on accounts with huge follower counts, because one post becomes tens of millions of writes and blocks the fan out queue for everyone else, so above a follower threshold I stop fanning out and merge those authors at read time instead. Feed lists store ids only, so edits and deletes take effect in one place, and unfollows and deleted posts are filtered during hydration rather than scrubbed from every list. Ranking sits on top as a separate stage, so it can change independently and degrade to reverse chronological if it fails.

IndGeek provides solutions in the software field, and is a hub for ultimate Tech Knowledge.