Skip to content

News feed

intermediateCommon atMetaGoogleAmazonLinkedInAnthropicRead firstHeapsCachingEvent-driven systemsPractise in the Sandbox →

A news feed shows each user the recent posts of everyone they follow. The core decision is when to do the merge: at write time (fan-out on write, push each post id into every follower’s precomputed list) or at read time (fan-out on read, merge followees’ posts on every load). Reads outnumber posts by 20× or more, so the standard answer is a hybrid: push for ordinary accounts, pull for the few with millions of followers. A feed load is then one cache read plus a merge of a handful of hot lists, O(k log k + page); a post costs O(followers) asynchronous list writes, capped by the celebrity threshold.

Facebook, Instagram, X/Twitter, LinkedIn and Threads all have one: open the app and see posts from accounts you follow, newest or most relevant first. Under the UI it is a many-to-many join between “who I follow” and “what they posted”, evaluated for hundreds of millions of users many times a day.

The goals conflict:

Goal What it means Pulls toward
Fast feed loads First page in ~200 ms, even for someone following 2,000 accounts Precompute (push)
Fresh feeds A new post shows up for followers within seconds Cheap, quick fan-out; or compute on read
Bounded write cost One post from an account with 100M followers must not stall the system Don’t push to everyone (pull)
Relevance Show the posts you care about, not just the newest A ranking stage, which needs candidates to rank
In scope (typical) Ask about
Functional Publish a post. Load the home feed of followed accounts, paginated. Deletes and edits show up. Chronological or ranked? Include reshares, ads, recommendations from accounts you don’t follow?
Scale 300M DAU, 0.5 posts/user/day, 200 average followers, 10 feed loads/user/day. The follower distribution: what’s the biggest account?
Latency Feed load p99 under ~300 ms. New posts visible to followers within ~5 s. Is “within a minute” acceptable for celebrity posts?
Consistency Eventual. Two followers may see a post a few seconds apart. Authors see their own post at once. Must a delete be instant everywhere? (Usually: hidden on next load.)
Availability Feed must load even when ranking or fan-out is degraded. Is a stale or unranked feed acceptable as a fallback?
Out of scope Post storage and media, search, notifications, the follow-graph service itself.

Drag the assumptions. The number to watch is fan-out writes per second: posts per second times followers. Then look at the last row: one celebrity post, pushed.

Back of the envelope
drag the assumptions
assumptions
so you need
posts per second1.74K/s
fan-out writes per second (pure push)347K/sone list push per follower per post
feed reads per second (peak ≈ 2×)69.4K/s
feed cache memory1.92 TB8-byte ids, before store overhead (often ≈2×)
one celebrity post, pushed at 1M writes/s1.7 min
300M DAU at 0.5 posts/day is about 1,700 posts/s. With 200 followers each that is ~350K feed-list writes/s, which a cache cluster handles. But one post from a 100M-follower account is 100 seconds of the whole fan-out tier.

What to say out loud: “About 1,700 posts a second become ~350K list writes a second on average, and feed reads are ~70K/s at peak. Feed lists of 800 ids are about 2 TB at 8 bytes per id, so they fit in a sharded cache. The average is fine. The problem is the tail: a celebrity post is a 100M-write burst. That’s why I’ll go hybrid.”

pseudocode: the contract
POST /posts { text, media_ids[] } -> 201 { post_id }
GET /feed?limit=20&cursor=... -> { posts[], next_cursor }
DELETE /posts/{id} -> 204
Post (key: post_id) { post_id, author_id, created_at, body, deleted }
Follows (key: follower_id) { followee_ids... } -- and the reverse index:
Followers (key: followee_id) { follower_ids... } -- paged, used by fan-out
FeedList (key: user_id) [post_id, ...] newest first, trimmed to ~800
AuthorList (key: author_id) [post_id, ...] that author's own recent posts

Two decisions hide in that sketch:

  • Feed lists hold ids, not posts. A post is stored once. Feeds are lists of 8-byte ids, hydrated at read time from a post cache. That makes edits and deletes a single-row change, and it keeps the feed cache small: 300M users × 800 ids × 8 B ≈ 2 TB, not petabytes.
  • Post ids are time-ordered. With Snowflake-style ids (timestamp in the high bits), sorting by id is sorting by time, and the cursor can be just (created_at, post_id). Merging lists needs no extra lookups.

Step through each flow. Notice that the write path ends in the feed cache and the read path starts there: the cache is the meeting point.

News feed: publish and read
step through
flow
A normal account posts. The work is done now, once, so every follower's read is cheap later.
push idpullClient (app / web): clientClientapp / webPost service (write path): servicePost servicewrite pathPost store (posts by id): storePost storeposts by idFan-out queue (post events): queueFan-out queuepost eventsFeed service (read path · ×N): workerFeed serviceread path · ×NFeed cache (user → post ids): cacheFeed cacheuser → post idsFan-out workers (×N): workerFan-out workers×NRanker (score, filter, mix): serviceRankerscore, filter, mixPost cache (hydrate ids): cachePost cachehydrate idsSocial graph (who follows whom): storeSocial graphwho follows whom
authorana
followers340
Ana publishes a post. The post service validates it and assigns a time-ordered id, so ids sort by creation time.
1 / 5
Component Role Why it’s there
Post service Validates, assigns a time-ordered id, stores the post once, emits an event. The write path stays short; fan-out happens after the response.
Fan-out queue + workers For each post, page through followers and push the id to each feed list. Async, retryable, horizontally scaled; absorbs bursts.
Social graph Followers and followees, both directions. Fan-out needs followers; the pull path and rebuilds need followees.
Feed cache One list of recent post ids per active user, sharded by user. The precomputed answer: a feed load is one key read.
Feed service Reads the list, pulls celebrity posts, calls the ranker, builds the cursor. Stateless; owns pagination.
Ranker + post cache Hydrates ids into posts, filters, scores, mixes. Relevance, and the place where deletes, blocks and privacy are enforced.

Pick a model, then change the follower skew and how many people are active today. The left chart shows total work; the right chart is the hybrid trade-off as you move the threshold.

Fan-out: who pays, the writer or the reader?
explore
model
cache ops per second at 200M accounts
push
79.9K/s
pull
673K/s
hybrid
157K/s
writes (store + fan-out)reads (feed loads)
hybrid: choosing T
0×1×9×10ms1s100s40.0K400K4.00M40.0Mthreshold T: pull authors with ≥ T followers
total cost vs pure pushslowest post to deliver (log)
model: 50.0K accounts, mean 58 followers, biggest 29.4Kbiggest single push: 3.96M lists ≈ 4 s at 1M writes/slists merged per feed load: 8.4accounts pulled: 147 (0.29%)
Push for the long tail, pull for the few accounts at or above T. Read the right chart from right to left: as T drops, the slowest delivery (red) falls by orders of magnitude while total cost (black) rises slowly at first, then steeply once readers must merge many lists. Pick the T where delivery time is acceptable. The pulled lists are few and very hot, so in practice they are served from cache and cost less than this count suggests.
Fan-out on write (push) Fan-out on read (pull) Hybrid
Cost of a post O(followers) list writes O(1) O(followers) only if below threshold T
Cost of a feed load 1 list read O(followees) reads + a k-way merge 1 list read + merge of a few celebrity lists
Freshness Seconds after fan-out finishes Always current Both
Worst case One celebrity post = 100M writes, minutes of lag Following 5,000 accounts = slow every time Bounded both ways
Wasted work Pushes to followers who never log in None Skip inactive followers too
Who used it Twitter’s original home timeline (Redis lists) Facebook’s feed was assembled at read time Twitter, Instagram and most large feeds today

Two refinements make push cheaper still:

  • Skip inactive followers. Most of a big account’s followers are not active on a given day. Push only to users seen in the last N days; rebuild the others with the pull model when they return (the “Cold user” flow).
  • Choose T by delivery time, not by follower count alone. If the fan-out tier writes 1M ids per second, a post to 5M followers takes 5 s. Pick T so the slowest push is still acceptable.

At read time the feed service merges the user’s precomputed list with the recent-post lists of the celebrities they follow. Each list is already sorted newest first, so this is a k-way merge with a heap of k heads (the heap page covers the structure). Step through it, then press next page.

Pull model: building one page from k followees
step through
ana
10:5610:5410:5310:48
bo
10:4710:4010:2710:2610:2410:1810:0710:029:45
cy
10:3810:2310:1010:069:569:539:429:33
heap
ana 10:56bo 10:47cy 10:38
page
ids fetched: 14ids used: 0heap size ≤ k = 3
in the heapnewest headon the pagebefore the cursor
Fetch the 5 newest ids from each of the 3 followees (no list can contribute more than one page): 14 ids fetched, in parallel.
1 / 8

What the stepper shows:

  • Fetch at most one page from each list. No single list can contribute more than limit items to a page, so you fetch k × limit ids and use limit of them.
  • The heap holds k items, so building a page costs O(limit × log k). With k ≈ 5 that is nothing; the cost is the k parallel fetches.
  • The cursor is (created_at, post_id) of the last item. Page 2 reads each list strictly after that pair. Offsets (?page=2) break as soon as a new post arrives at the top: everything shifts down one, and the reader sees a duplicate.

The fan-out side is a simple loop. What matters is that it pages, skips, and can be retried safely:

pseudocode: fan-out worker for one post event
on post_created(post_id, author_id):
if follower_count(author_id) >= CELEBRITY_T:
append post_id to AuthorList(author_id) -- readers will pull it
return
for page in followers(author_id), 1,000 at a time:
for user in page where last_active(user) > now - 30 days:
push post_id onto FeedList(user) -- pipelined, ~1 round trip per page per shard
trim FeedList(user) to 800
-- retry-safe: pushing the same id twice is harmless if reads dedupe by post_id

Ranked feeds change pagination. If scores change between page 1 and page 2, a cursor on (score, id) skips or repeats posts. The usual fix is to rank once per session: compute the ordered list of a few hundred candidate ids, store it under a session key with a short TTL, and let the cursor be an offset into that frozen list.

Ranking turns “recent posts from people you follow” into “posts you’ll engage with”. It runs as a funnel, because scoring thousands of posts with a large model per feed load is too slow:

  1. Candidate generation. The merged list (a few hundred to ~1,500 ids), plus optional out-of-network recommendations.
  2. Light ranker. A cheap model or heuristic cuts candidates to a few hundred.
  3. Heavy ranker. A large model predicts engagement (like, reply, dwell time) for each survivor.
  4. Filters and mixing. Drop deleted, blocked, muted and already-seen posts. Enforce author diversity (not five posts in a row from one account), insert ads.

X’s open-sourced recommendation code describes exactly this shape: candidate sources, a light and a heavy ranker, then filtering and mixing.

Edits, deletes, unfollows and blocks are all handled at read time. Feed lists hold ids; hydration reads the current post and the current relationship, and drops anything that no longer qualifies. A background job may remove stale ids later, as cleanup. Rewriting millions of lists on every delete would be a fan-out storm in reverse.

pseudocode: hydrate and filter a page of candidates
hydrate(viewer, ids):
posts <- post_cache.multi_get(ids) -- one batched round trip per shard
keep p where p exists and not p.deleted
and not blocked(viewer, p.author) and not muted(viewer, p.author)
and visible_to(viewer, p) -- privacy rules, checked now, not at fan-out time
return keep

Hot spots. A celebrity’s AuthorList is read by every follower on every feed load, so it is the hottest key in the system. Replicate it to every cache shard, or cache it in each feed-service process for a second or two. A viral post’s body is likewise hot in the post cache. The distributed cache deep dive covers hot-key replication.

What breaks What you see Design response
Fan-out backlog (celebrity, spike) New posts reach followers minutes late Hybrid threshold; partition the queue; prioritise active users; autoscale workers
Feed cache shard lost Affected users get a miss Rebuild on demand with the pull model; replicas; the cache is derived data, never the source of truth
Ranker slow or down Feed latency spikes Timeout, then fall back to the chronological merge; serve stale ranked results
Duplicate fan-out after a retry The same post twice in a feed Dedupe by post_id at read time; or make pushes idempotent per (user, post)
Hot celebrity list One cache shard at 100% Replicate hot keys; local in-process cache with a 1-2 s TTL
User follows 5,000 accounts Rebuild is slow Cap the pull on rebuild (most recent N followees by activity); async rebuild then refresh
Deleted post still shown Privacy and trust problem Filter at hydration; invalidate the post cache on delete
Operation Time Space
Publish (store + enqueue) O(1), ≈10 ms Post body once
Fan-out of one post O(followers) list pushes, async; ≈ followers / 1M per second of fan-out capacity 8 B per follower per post (until trimmed)
Feed load, push part O(1) list read + O(limit) hydration —
Feed load, pull part k parallel fetches + O(limit × log k) merge O(k × limit) ids in memory
Rebuild a cold feed O(followees) fetches + merge, ≈100s of ms Writes one list
Feed cache total — DAU × list length × 8 B ≈ 2 TB at 300M × 800
Fan-out writes ≈ posts/s × average followers ≈ 350K/s here —

A good 45-minute arc:

  1. Clarify (5 min). Chronological or ranked, follower distribution, freshness target, scale.
  2. Estimate (5 min). Posts/s, fan-out writes/s, feed reads/s, feed-cache memory, and the celebrity burst.
  3. Data model (5 min). Posts stored once, feed lists of ids, time-ordered ids, both directions of the graph.
  4. Push vs pull (10 min). Explain both and their costs. Land on hybrid with a threshold and skipping inactive users.
  5. Read path (10 min). Merge, cursor pagination, hydration and filtering, ranking funnel.
  6. Harden (5-10 min). Fan-out backlog, cache loss and rebuild, ranker fallback, hot keys, deletes.

Follow-ups to expect: “A user follows a celebrity: when does that celebrity’s old content appear?” Immediately, because celebrity posts are pulled at read time; for a normal account, backfill its recent posts into the follower’s list on follow. “Make the feed multi-region.” Fan-out writes into a feed cache in each region from a replicated post log; each region serves its own reads. “How do you know ranking helps?” A/B test on engagement and on long-term retention, not just clicks.

Check yourself
pick one
Your feed uses pure fan-out on write. An account with 80M followers posts. Fan-out workers together push 1M ids per second. Roughly how long until the last follower can see the post, and what's the usual fix?
Check yourself
pick one
A user deletes a post that is already in 300 followers' feed lists. What's the cheapest correct way to make it disappear?
Check yourself
pick one
The feed API uses ?page=2 with offsets of 20. Users report seeing the same post at the bottom of page 1 and the top of page 2. Why, and what fixes it?

Concept pages to drill: Heaps · Caching · Event-driven systems · Iterator design · Graph representation · Distributed thinking