Library
29 min read

Design Instagram feed

Open the app and the newest posts from people you follow are already there. That list is your feed. Reading it has to feel instant, because nobody waits for a feed. Writing a post is different: it can take a few seconds to reach everyone, and the person who posted will never notice, because they already know what it says.

That difference is the whole design. Everything below moves work away from the moment someone reads and onto the moment someone posts. Then it deals with the one case where that stops working. Watch one post travel from the author to a follower.

ClientLoad balancerPost serviceFan out queueFan out workersFollower graphFeed storeFeed servicePost store

Post. An author taps share. The request reaches the load balancer, which picks a server to handle it.

Three words come up all through this page:

  • Fan out means copying one thing to many places. One post fans out to every follower's feed.
  • Precompute means doing the work early, before anyone asks, so the answer is ready.
  • Hydrate means turning a list of ids into the real posts by looking each id up.
The real Instagram

This page builds the home feed of an app like Instagram. You follow accounts, a few accounts have tens of millions of followers, and opening the app shows posts from the people you follow. The numbers and choices below are interview assumptions, not Instagram's own. One real difference: Instagram has ranked its feed, rather than showing newest first, since 2016. That is exactly why this page keeps fetching the feed and ranking it as two separate layers.

Step 1: Understand the problem#

Six questions. One answer below decides the whole architecture and one of them is a trap. Find both before you draw anything.

You askThey sayWhat it settles
How many daily users, and how often does each one open the feed?About 300 million, roughly ten opens a day.How many reads there are for each write, and it is huge: tens of thousands of feed reads a second against a few thousand posts. Anything expensive has to move to the write side.
How fresh does a feed have to be?A few seconds behind is fine. It must never be empty or out of order.Permission to precompute. If every follower had to see a post the instant it was made, the work could not be done in the background, and every read would have to build the feed from scratch.
Chronological or ranked?Newest first for now.Fetching the feed becomes reading a sorted list of ids. Say you would keep fetching and ranking as separate layers, because ranking always arrives later and you do not want to rebuild the fetching part when it does.
What is the largest follower count?Tens of millions.The trap, and the second half of this page. One post turning into 40 million writes breaks the obvious design, and this answer is the warning.
Is a follow allowed to take effect a minute later?Yes.Follows can be handled in the background too, so no request ever has to rewrite a feed list while a user waits.
Do we store the photos and videos, or a reference to them?A reference. Someone else owns the media pipeline.Feed entries stay tiny, tens of bytes each, which is what makes it affordable to keep whole feeds in memory.

"A few seconds behind is fine" has a name: eventual consistency. Followers will all see the post, just not at the same instant. The strong vs eventual consistency article shows the difference live. Getting this answer is what makes the rest of the page possible.

What you are building#

Functional requirements
What the system does.
  • Post something. Text and a media reference. The media pipeline is somebody else’s design question.
  • Follow and unfollow. One way only: you can follow someone who does not follow you back. So a follow is not a friendship.
  • Read your feed, newest first, a page at a time. This is 99% of the traffic and all of the speed budget.
Non-functional requirements
How well it has to do it. The numbers you commit to.
  • Feed loads in under 200ms for 95 out of 100 requests (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.
Out of scope
What you cut, and say so out loud.
  • Ranking and personalisation. Huge, and it sits on top of this rather than inside it. Say you would keep fetching separate from ranking.
  • Media upload, transcoding and delivery.
  • Notifications, which look similar and are a separate fan out problem.
  • Ads, which are mixed into the feed at read time.

p95 means the 95th percentile: sort all request times, and 95% of them must be under 200ms. It is a better target than the average, because the average hides the slow requests real people feel. Tail latency at scale explains why the slow few matter so much.

Back of the envelope#

Before drawing boxes, count the work. (The method is in back of the envelope.) Here is a small version first, so the slider numbers below feel real.

Say Meera has 200 followers and posts one photo. If we precompute feeds, that one post becomes 200 small writes: put this post's id at the top of follower 1's list, follower 2's list, and so on. Now do that for everyone who posts today.

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 each would be one simple lookup. The fan out turns one post into 200 writes, so the write side now does 10.0 times the work of the read side. That is the trade: you buy a fast read with an expensive write, and it works until one user has far more followers than the average.

Step 2: Propose the high level design#

The API#

Three calls cover everything in scope.

GET/v1/feed?cursor=eyJ0IjoxNzU2...&limit=20
returns 200 { items: [...], nextCursor: "..." }
Why: Cursor pagination, not offset. A feed keeps moving as new posts arrive, so "give me items 21 to 40" would show you items you already saw on page 1. A cursor is a bookmark: "give me 20 items older than this one".
POST/v1/posts
{
  "text": "...",
  "mediaId": "m_88123",
  "clientId": "c_4f21ab"
}
returns 202 { "postId": "...", "state": "FANNING_OUT" }
Why: 202 Accepted, because the post is saved 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 changes what you see from now on, not what you saw before. Whether a new follow should pull that person's old posts into your feed is a product decision worth asking about out loud.

An idempotency key is a unique id the app attaches to one action. If the same request arrives twice because the phone retried, the server sees the same key and does the work only once. The idempotency keys article lets you try it.

The data model#

Two tables. The first is the truth. The second is a copy shaped for fast reading.

postssharded by post_id, time ordered ids
post_idbigintPKSnowflake style, so it sorts by time without a separate 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 from it and can be rebuilt.

A Snowflake style id is a 64 bit number that starts with the time it was made, so sorting ids also sorts posts by time. How to build one without two servers handing out the same number is a whole question: design a unique ID generator.

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: two round trips, and both are cached.

The feed table lives in Redis, an in-memory store where reading a list takes about a millisecond. Keeping a ready made answer in memory in front of the real data is caching, and caching patterns covers how it fails. "Sharded by user_id" means the users are split across many machines, each machine holding some of them; the sharding chapter starts from zero.

The cap of about 800 matters. Nobody scrolls past a few hundred items, so trimming the list keeps storage bounded and the write cheap. If someone does scroll to the end, build older pages on demand. That is rare enough to be allowed to be slow.

The whole system on one whiteboard#

Figure 1 is the picture at the top of the page, with the last few pieces added.

Figure 1. The top row is one post being written into millions of feeds and is allowed to take seconds. The bottom row is a feed being read and gets 200ms. The two halves meet only at the feed store.

Walking Figure 1:

  1. A request arrives at the load balancer, which spreads requests across many identical servers so no single one is overloaded. (Load balancing covers how it picks.)
  2. For a new post, the post service writes it once, to the post store, and returns. The author is not waiting for anything else.
  3. It drops a small job on the fan out queue: just the post id. A queue is a waiting line between two parts of the system. The post service puts work in, workers take it out whenever they are free. Here it is Kafka, split into partitions by post id. The message queues chapter explains partitions, and queue vs pub/sub vs event stream explains where Kafka sits among them.
  4. Fan out workers pick the job up and ask the follower graph who needs to see it.
  5. For each follower, the worker pushes the post id onto that follower's list in the feed store. This is the expensive step, and it is the reason the queue sits in front of it.
  6. On the other side, a feed read is a list lookup. Ids come out of the feed store already in order.
  7. The feed service merges in anything that was deliberately not fanned out. That is how accounts with tens of millions of followers are handled, explained two sections down.
  8. The ids are hydrated into posts in one batch call to the post store, photos and videos come from the media CDN (servers near the user that hold copies of the files), and the response goes out.

The queue does more than separate the two sides. It soaks up the burst when a popular account posts. It lets fan out be retried on its own if it fails, without touching the post. And it means the post is safely stored before any of the expensive work starts.

Your answer

Step 5 of the walk above is one write per follower. A celebrity with 40 million followers posts. What exactly goes wrong, and would adding workers fix it?

Step 3: Design deep dive#

Push, pull, or both#

Think of a newspaper. Push is the paper boy who drops a copy at every subscriber's door the moment it is printed: reading is instant, delivery is a lot of walking. Pull is you walking to every shop that sells the papers you like, each morning, and reading them there: nothing is delivered, but your morning is slow. Feeds face the same choice. The push vs pull article puts this case next to four others.

When someone opens the app, where does their feed come from?

  1. First try
    Build it when they open the app (pull, or fan out on read)
    Store nothing extra. When a user opens the app, look up the 200 accounts they follow, fetch each one's recent posts, merge them newest first, and return the top 20. Posting costs nothing, and no work is wasted on users who never open the app.
    Where it breaks: Every single feed open becomes 200 lookups that must all come back before the page can show. At 35,000 opens a second that is millions of lookups a second, and the slowest of the 200 decides how long the user waits. The 200ms budget is gone.
  2. Better
    Build it when someone posts (push, or fan out on write)
    When Meera posts, write her post id into the precomputed list of each of her 200 followers. Opening the app is now one list read plus one batch hydrate. This is as fast as a read can be.
    Where it breaks: It assumes follower counts are modest. An account with 40 million followers turns one post into 40 million writes, which jams the queue for everybody. The next two sections are about exactly this.
  3. What you ship
    Push for almost everyone, pull for the few huge accounts (hybrid)
    Below a follower threshold, push. Above it, do not fan out at all. When a user opens the app, read their precomputed list and merge in the recent posts of the few huge accounts they follow. That merge touches a handful of accounts, not 200, so it stays fast.

Here are the three shapes side by side.

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.

Why the celebrity breaks it#

Fan out on write is sized from the average follower count. The trouble is that follower counts do not cluster around an average. They follow a power law: most accounts have a handful of followers, a few have millions, and the few at the top are so far out that the average describes nobody. Switch between the two views below.

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.

The first chart says "fan out is cheap". The second says "a few accounts do most of the work, all at once". A design sized from the first is broken by the second.

Make it concrete. An account with 40 million followers posts once. That is 40 million list writes from one request, and it lands as one burst.

An account with 40 million followers just posted. What do you do?

  1. First try
    Add more fan out workers
    The queue is backing up, so add machines to drain it faster.
    Where it breaks: You are not short of machines, you are short of a reason to do 40 million writes. The work is real. While it runs, everyone else's posts sit in the queue behind this one (head of line blocking), and the last follower sees the post minutes after the first.
  2. Better
    Give big accounts their own queue partition
    Route posts from large accounts to a separate partition with its own workers, so a big post cannot block normal posts.
    Where it breaks: Normal users are safe again, which is worth keeping. But the 40 million writes still happen, still cost the same, and the celebrity's followers still get the post over minutes.
  3. What you ship
    Do not fan out above a threshold. Merge at read time instead
    Write the celebrity post once and stop. When a follower opens the app, the feed service fetches recent posts from the few huge accounts they follow and merges them into the precomputed list. Keep the separate partition for accounts that are large but below the threshold.
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, measure 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.

Keeping the feed correct#

The feed lists are derived data: a copy built from the posts and follows tables. Copies drift away from the truth over time, so plan for the four ways this one drifts.

Unfollow. Their posts are already sitting in your list. The usual answer is to filter at read time: when hydrating, you have each post's author id anyway, so drop posts from people you no longer follow. Unfollows are rare, so this costs almost nothing.

Deleted posts. Same shape. The id stays in feed lists, and hydration finds nothing for it. So the read path has to skip gaps and fetch a few extra ids to fill the page.

A new follow. Your list has none of that person's older posts. Either copy their recent posts into your list (backfill), or let the feed fill in going forward. Backfilling feels nicer, and it is a product decision worth naming.

Rebuilding. Treat feed lists like a cache whose miss is expensive. You need a job that can rebuild one user's feed from the posts and follows tables, for when a list gets corrupted or the retention rules change. If you cannot rebuild it, the copy has quietly become a second source of truth, and now you have two.

Break it#

Push the load up one level at a time and watch which box goes red first.

Fan out load
typical
typicalpopular postcelebrity postshybrid
Healthy. Average author, 200 followers. About 350,000 feed writes a second across the fleet, spread evenly. The queue is nearly empty and posts land in followers’ feeds in under a second.
Read every step as text
  1. typical · Healthy. Average author, 200 followers. About 350,000 feed writes a second across the fleet, spread evenly. The queue is nearly empty and posts land in followers’ feeds in under a second.
  2. popular post · Degraded. An account with 200,000 followers posts. One request becomes 200,000 writes. The workers absorb it and the queue drains in a few seconds, but everyone else’s posts are slower because they are queued behind it.
  3. celebrity posts · Down. 40 million followers, one post. The queue fills with one message’s worth of work and every other post in the system waits behind it. The last follower sees the post minutes after the first. More workers do not help, because the work is real and the queue is blocked at the front.
  4. hybrid · Healthy. Above the threshold, do not fan out at all. The post is written once and merged into feeds at read time for the people who follow that account. Fan out load is back to normal, celebrity posts appear right away instead of over minutes, and the read side pays a small, bounded cost.

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 gathers from 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 cope with 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 gap 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 order 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. Fetching 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 storage, and a ranking outage falls back to newest first 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?

Worth memorising
  • 300 million users opening ten times a day is about 35,000 feed reads a second, against a few thousand posts.
  • One post from an account with 40 million followers is 40 million list writes. Adding workers does not fix it.
  • A feed entry is an id and a timestamp, tens of bytes. That is why whole feeds live in memory.
  • Push for everyone, pull for the few hundred accounts big enough to break it. The hybrid is the answer.
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 fall back to newest first if it fails.