Design a News Feed
Design a social news feed: fan-out on write vs on read, the hybrid for huge accounts, feed storage, cursor pagination, ranking and caching.
On this page
You open a social app and see posts from the people you follow, newest or most interesting first. Scroll down and older ones load. Post something and your followers see it within seconds. Behind that screen is one hard question: when someone posts, do you do the work then, copying the post into every follower's feed, or later, gathering posts when each follower opens the app?
Interviewers like it because the two obvious answers fail at scale in opposite directions, and the fix is a hybrid you must justify with numbers. It also tests caching and pagination. It follows A Framework for Any Design.
Clarify the problem
Pin down what the feed contains, how it is ordered, and the shape of the follow graph. The last one decides the design.
Design the news feed for a social app.
Is the feed only posts from accounts I follow, or also recommendations and ads? And is it chronological or ranked?
Only accounts you follow. Ranked, but start chronological and add ranking later.
How many daily users, how often do they open the feed, and how often do they post?
100 million daily users. Each opens the feed about 10 times a day. About 10 million posts a day.
How are follower counts spread? Is there a cap, or can one account have tens of millions of followers?
The average is about 200 followers, but some accounts have 50 million.
Then the feed read is the hot path and must be fast. A new post can take a few seconds to appear in feeds, and a feed that is briefly missing a post is fine. Those huge accounts will need special handling. I'll leave out posting media, search and notifications.
- Functional: publish a post; see a feed of posts from followed accounts, newest first at launch and ranked later; scroll back through older posts.
- Non-functional: the feed loads in under 200 ms at p99 on our side; it stays available when parts fail; a new post reaches followers' feeds within about 5 seconds; feeds may be eventually consistent.
- Out of scope: media upload, search, notifications, ads, recommendations from accounts you do not follow.
Estimate the scale
- Feed reads: 100 million × 10 = 1 billion a day. 109 ÷ 86,400 ≈ 11,600 a second; call it 12,000. Assume a 3x peak: 36,000 a second.
- Posts: 10 million a day ÷ 86,400 ≈ 116 a second; about 350 at peak. Reads outnumber posts 100 to 1.
- Post storage: about 1 KB of text and metadata per post (media lives in a blob store), so 107 × 1 KB = 10 GB a day, about 3.7 TB a year.
- Read bandwidth: a page of 20 posts at 1 KB is 20 KB; 36,000 × 20 KB = 720 MB/s at peak.
If every post is copied into each follower's feed, how many feed inserts per second is that on average?
- 10 million posts a day
- 200 followers per account on average
- A day is 86,400 seconds
Show the working
- Inserts a day: 107 × 200 = 2 × 109
- 2 × 109 ÷ 86,400 ≈ 23,000 a second
- At a 3x peak: about 69,000 a second
Estimate 23 thousand/s (anything from 8 thousand/s to 70 thousand/s is a good answer)
That is manageable. The average hides the problem: one post from a 50-million-follower account is 50 million inserts at once.
How much memory to keep the newest 500 feed entries for every daily user?
- 100 million daily users
- Keep the newest 500 entries per feed
- Each entry is a post id and an author id, 8 bytes each
Show the working
- One feed: 500 × 16 B = 8 KB
- All feeds: 108 × 8 KB = 8 × 1011 B = 800 GB
Estimate 800 GB (anything from 300 GB to 3 TB is a good answer)
That is raw data. In-memory sorted structures add per-entry overhead, often several times the 16 bytes, so plan for a few TB across a few dozen cache nodes. Storing ids, not whole posts, is what keeps it this small.
Write the endpoints for posting and for reading the feed. How does the client ask for the next page?
Compare with a model answer
POST /v1/posts and GET /v1/feed with a limit and a cursor. The decision to argue is cursor versus page-number pagination: in a feed where new posts keep arriving at the top, page numbers return duplicates.
Define the API
POST /v1/posts
body: { "text": "...", "media_ids": ["m_91"]? }
201 Created { "post_id": "7311045529", "created_at": "..." }
400 Bad Request empty or too long
429 Too Many Requests posting too fast
GET /v1/feed?limit=20&cursor=opaque-string
200 OK { "posts": [ { "post_id", "author", "text", "media_urls", "created_at", "like_count" } ],
"next_cursor": "opaque-string" or null }
400 Bad Request cursor expired or invalid: start again from the top
POST /v1/users/{id}/follow
DELETE /v1/users/{id}/followThe decision worth arguing is cursor pagination. With ?page=2, the server skips the first 20 items, but if 3 new posts arrived since page 1, items 18 to 20 of the old page 1 show up again. A cursor says "continue after this exact item", so new posts at the top cannot shift the next page. The cursor is opaque to the client, so its contents can change when ranking arrives. The deep dive below shows both.
List the records: posts, who follows whom, and the feed itself. For each, what is the main lookup, and what kind of store fits?
Compare with a model answer
Posts are looked up by id. Follows are looked up both ways: "who follows X" for fan-out and "whom does Y follow" for reads. The feed is a short list of post ids per user, read on every open: an in-memory list, not a table.
Model the data
posts post_id BIGINT PRIMARY KEY -- time-sortable id
author_id BIGINT
text VARCHAR(2000)
media_ids LIST
created_at TIMESTAMP
deleted BOOLEAN
followers (author_id, follower_id) -- "who follows X": fan-out
following (follower_id, author_id) -- "whom does Y follow": reads
author_posts author_id -> newest post ids -- for pull and rebuilds
feed user_id -> set of (post_id, author_id), sorted by post_id, max 500Posts are written once, read by id, and partitioned by post_id: a wide-column or key-value store fits, with a cache in front for hot posts. A post id that sorts by time (see Unique IDs) lets you order feeds by id alone, no timestamp lookups.
The follow graph is stored twice, once per direction, each partitioned by its first column, so both questions are single-partition reads. Keeping two copies in step is the cost; write both in the follow request and repair with a background job.
The feed lists live in an in-memory store with replicas, such as Redis. They hold ids, not posts, so an edit or delete never has to touch 50 million feeds. And they are derived data: if a list is lost, rebuild it from following and author_posts.
Sketch the write path (a post reaching followers' feeds) and the read path (a feed page reaching the app). Which parts are synchronous?
Compare with a model answer
The post is stored synchronously; copying it into feeds happens asynchronously through a queue and fan-out workers. The read path reads one feed list from cache and fills in the posts. The next section draws it.
High-level design
Writes and reads are separate services. The post service stores the post and publishes an event; fan-out workers do the slow copying into feeds. The feed service reads one list, adds posts from the few huge accounts the user follows, and fills in post bodies from the post cache.
request and replyasynchronous message
1Maya, who has 200 followers, posts a photo caption. The post service gives it a time-sortable id and stores it.
2It publishes a "new post" event and replies 201 to Maya at once. She does not wait for 200 feeds to be updated.
- followers
- 200
- active
- 170
3A fan-out worker takes the event and asks the follow graph for Maya's followers: 200 ids. It skips the 30 who have not opened the app in a month; their feeds are rebuilt if they return.
- followers
- 200
- active
- 170
- inserts
- 170
4It adds
p_901to 170 feed lists, in batches, and trims each to 500. About 2 seconds after Maya posted, her followers can see it.- page
- 1
- ids
- 20
5Leo, one of those followers, opens the app. The feed service reads the first 20 ids of his list, and his cached following list.
- page
- 1
- ids
- 20
- merged from
- 1 + 3 lists
6Every feed server keeps the set of huge accounts in memory; 3 of them are in Leo's following list. Their posts were never copied into his list, so the service reads their newest post ids, cached because millions of readers need them, and merges by id.
- page
- 1
- ids
- 20
- cache hits
- 20
7It fetches the 20 post bodies by id from the post cache in one batch, drops any that were deleted, and builds the page.
- page
- 1
- returned
- 20
- next_cursor
- after p_655
8Leo gets 20 posts and a cursor pointing at the last one. A few cache reads, no database. That is how the read stays well under 200 ms.
9Failure: the fan-out workers fall behind and the queue backs up. Reads still work; new posts just appear late. Posting still works too, because the queue absorbs the backlog.
Deep dives
Fan-out on write, on read, or both
This is the question interviewers care about most. Fan-out on write (push) copies each new post's id into every follower's feed when it is posted. Fan-out on read (pull) stores nothing per reader: when you open the app, it fetches the newest posts of everyone you follow and merges them.
Put numbers on both. Pull for everyone means each feed open reads about 200 author lists: 36,000 × 200 = 7.2 million list reads a second at peak. Push for everyone means 69,000 inserts a second at peak, which is fine, until a 50-million-follower account posts. If the fan-out fleet can do 200,000 inserts a second, that one post takes 5 × 107 ÷ 2 × 105 = 250 seconds and delays every other post behind it.
| Option | Good at | Bad at | Pick it when |
|---|---|---|---|
| Fan-out on write (push) | Reads are one list lookup: fast and cheap. Most work happens once per post. | A huge account's post means tens of millions of writes. Wasted work for followers who never log in. | Follower counts are small and capped, like a private friends network. |
| Fan-out on read (pull) | Posting is one write. No wasted work for inactive users; huge accounts cost nothing extra. | Every feed open merges hundreds of lists. Slow, and read load is 100 times write load. | Reads are rare compared to posts, or users follow very few accounts. |
| Hybrid Usual default | Push for normal accounts, pull for accounts above a follower threshold. Both costs stay bounded. | Two code paths, and a threshold to tune. Accounts that cross it need care. | Follower counts are heavily skewed. The usual answer here. |
Pick a threshold, say 100,000 followers, and say what decides it: below it, a post costs at most 100,000 inserts, half a second of the fleet. Above it, the account's posts are pulled at read time. If a typical user follows a handful of such accounts, a feed read merges one pushed list with a few pulled lists, all from cache. What would change my mind: if users followed hundreds of huge accounts each, the pulled side would dominate, and I would lower the threshold or precompute merged feeds for the most active readers.
Also skip inactive users when pushing. Someone who has not opened the app in 30 days gets no inserts; when they return, build their feed by pull once, then switch them back to push.
Feed storage and pagination
Each feed holds up to 500 ids, kept as a set sorted by post id, not a list you prepend to. Fan-out for two authors can finish out of order, and a queue delivers an event again after a worker crash; a sorted set puts late posts in place and ignores repeats (see Idempotency and Retries). Readers rarely scroll past a few pages; anyone who goes past 500 falls back to pulling from author_posts. Watch why page numbers break when posts keep arriving.
- feed090188285381477574670766
- page
- 1
- shown
- 90 88 85 81
1Leo's feed, newest first. Page size 4 to keep it small. Page 1 is positions 0 to 3: posts 90, 88, 85, 81.
- feed095193290388485581677774870966
- page
- 1
- shown
- 90 88 85 81
2While he reads, 2 new posts arrive at the top: 95 and 93. Everything else shifts down 2 places.
- feed095193290388485581677774870966
- page
- 2
- shown
- 85 81 77 74
- duplicates
- 2
3With
?page=2the server skips 4 and returns positions 4 to 7: 85, 81, 77, 74. Leo sees 85 and 81 twice. - feed095193290388485581677774870966
- cursor
- after 81
- shown
- 77 74 70 66
- duplicates
- 0
4With a cursor, page 1 ended with "after 81". The server returns the next 4 ids smaller than 81: 77, 74, 70, 66. No duplicates, whatever arrived on top.
That works because ids sort by time. A ranked feed has no such order: scores change as posts get likes. So when ranking arrives, the feed service ranks once when the session starts, caches that ordered list for about 10 minutes, and the cursor becomes "snapshot id + position". If the snapshot has expired, the API returns 400 and the app starts from the top. That is why the cursor is opaque.
Ranking, in outline
The interview needs ranking's shape, not its model: a stage the feed service calls, with a time budget.
- Candidates: the ~500 ids from the pushed list plus the pulled lists.
- Filter: drop deleted posts, blocked authors, posts already seen.
- Score: a model predicts how likely the reader is to engage, from features like how often they interact with the author, the post's age and its early engagement.
- Re-rank for variety: no more than 2 posts in a row from one author.
Give ranking about 50 ms of the 200 ms budget. If the ranking service is slow or down, return the chronological feed instead: a worse feed is better than no feed.
Cache strategy
Four caches do the work, each for a different reason:
- Feed lists (800 GB): the pushed feeds, replicated. Losing a node means rebuilding those users' feeds by pull, so warm them gradually.
- Post objects: hydration reads by id. Posts from the last two days are about 2 × 10 GB = 20 GB and take most reads, so they fit easily. Use cache-aside and delete the entry on edit or delete.
- Recent posts of huge accounts: read by millions of feeds. Copy these hot keys into each feed server's memory for a few seconds so one cache node does not take all those reads.
- Follow lists, both directions: followers for the fan-out workers, following for the feed service, so neither hammers the graph store.
Like counts change constantly; keep them out of the post object, in a separate counter that is cached briefly and updated in batches. Otherwise every like invalidates a hot post.
Bottlenecks and failures
At 10x the traffic (360,000 feed reads and about 690,000 fan-out inserts a second at peak), the raw feed data grows to about 8 TB, which means a larger cluster partitioned by user id with consistent hashing. The fan-out queue is partitioned by author so workers scale out. The first things to hurt are the hot keys of huge accounts, which the local in-memory copies handle, and the fan-out fleet, which you can relieve by lowering the threshold.
| When this dies | What happens | What you do |
|---|---|---|
| A feed cache node | Its users' feed lists are gone. | Replica takes over; if both are lost, rebuild those feeds by pull, slower for a while. |
| Fan-out workers | New posts stop reaching feeds; the queue grows. | Reads and posting continue. Workers catch up from the queue, oldest first. |
| The post store | New posts fail; hydration misses fail. | Most reads hit the post cache. Return what is cached rather than an error page. |
| The ranking service | No scores. | Serve the chronological feed. |
| The follow graph | Fan-out stalls; follows fail. | Cached follower lists keep fan-out running for a while; the queue holds the rest. |
What a strong answer sounds like
| Level | What the answer does |
|---|---|
| Junior | Builds the feed with a query over posts from followed users at read time. Works, but needs prompting to see the cost at 36,000 reads a second. |
| Mid | Proposes fan-out on write with a feed cache, uses a queue so posting stays fast, and paginates with a cursor. |
| Senior | Asks about the follower distribution up front, computes the cost of both fan-outs, picks a hybrid with a justified threshold, skips inactive users, stores ids not posts, plans for ranking with a fallback, and says what happens when each part fails. |
An account with 50 million followers posts. Walk me through it.
With pure push that is 50 million inserts; at 200,000 a second, about 4 minutes during which other posts queue behind it. So accounts above 100,000 followers are not pushed. The post goes to that account's recent-posts list, which is cached and copied into each feed server's memory. Followers' feeds merge it at read time.
What does that cost on reads?
A few extra cached list reads per feed open, if the user follows a few such accounts. If one user followed hundreds, I would cap how many pulled lists we merge and push for the rest.
Follow-up questions
What happens when I unfollow someone?
Remove that author's ids from your feed list; it holds at most 500 entries, so the scan is cheap. A fan-out already in flight may add one more post afterwards, so the read path also drops ids whose author is not in your following list, which it reads anyway.
How do you handle a deleted post?
Mark it deleted and delete its cache entry. Feed lists still hold the id, and hydration drops it. Nothing touches millions of feeds.
How does the app show "5 new posts"?
Every minute or so, the app asks for the count of ids above its newest one. That is a cheap read of the feed list. Pushing over an open connection is possible but costs a connection per active user.
How would you run this in two regions?
Serve feeds from each region's caches and stores. Replicate posts and follows between regions asynchronously, and run fan-out in each region for the users it serves. A post may reach the other region's feeds a second or two later.
An account crosses the 100,000 threshold. What changes?
New posts switch from push to pull. Its recent posts already sit in its followers' lists, so nothing has to move; the read path merges both and removes duplicates by id.
After your attempt, tick what you really did. Be strict: "I thought about it" is not "I said it out loud".
Before drawing
Numbers and interfaces
Design and depth
Failure
What to practise next
The fan-out queue here is a stream with partitions and consumer groups; see Message Queues and Streams. For a design where delivery must be instant rather than pulled, try Design a Chat App.