SYSTEM DESIGN · DESIGN PROBLEMS · FEEDS AND SOCIAL · MEDIUM · 11 OF 41

Design a News Feed: Fan-Out, Inboxes and a Ranking Pipeline

How to design a ranked social news feed for a billion daily users: scope, traffic and memory math for per-user inboxes, the feed API and data model, fan-out of friend posts through an event stream, candidate generation and multi-stage ranking, privacy filtering at read time, seen-state and new-post signals, and the trade-offs you should name.

Reported atMetaLinkedInSalesforceWalmart Global Tech

Try it on the practice board Drag the blocks, wire them up, and check your design against the reference.

  1. 1
  2. 2
  3. 3
  4. 4
  5. 5
  6. 6
  7. 7
  8. 8
  9. 9
  10. 10
  11. 11
  12. 12
  13. 13
  14. 14
  15. 15
  16. 16
  17. 17
  18. 18
  19. 19
  20. 20
  21. 21
  22. 22
  23. 23
  24. 24
  25. 25
  26. 26
  27. 27
  28. 28
  29. 29
  30. 30
  31. 31
  32. 32
  33. 33
  34. 34
  35. 35
  36. 36
  37. 37
  38. 38
  39. 39
  40. 40
  41. 41
1 / 18

Press play to hear the narration, or step through at your own pace.

Notes

Listen

Requirements. A user can publish a post, and sees a home feed built from friends, pages they follow and groups they belong to. The feed is ranked by relevance, not purely by time, and scrolls endlessly. We'll leave out ads, comment threads, chat and media processing, which has its own design. Confirm with the interviewer that ranking is in scope, because it changes the shape of the answer.

Non-functional requirements. A page of feed should come back in under three hundred milliseconds at p ninety-nine. A new post should reach friends within about ten seconds, so eventual consistency is fine. One rule is strict: never show a post the viewer isn't allowed to see. And plan for degradation: if the ranking service is slow, fall back to recent posts rather than failing.

Functional requirements
  • in scope:
  • • publish a post (text, link,
  • photo reference)
  • • home feed from friends,
  • followed pages, groups
  • • ranked, not just by time
  • • infinite scroll
  • out of scope:
  • • ads, comments threads, chat
  • • media processing details
Non-functional requirements
  • p99 feed page: under 300 ms
  • new post visible to friends
  • within ~10 seconds
  • never show a post the viewer
  • is not allowed to see
  • available over consistent
  • graceful degrade: if ranking
  • is down, show recent posts

Ask scale

Assume a billion daily users loading the feed ten times a day: ten billion loads, about a hundred thousand requests a second, double that at peak. Half of users post once a day, so five thousand posts a second. Here's the number that matters: with an average audience of three hundred, pushing each post to every inbox is a hundred fifty billion inbox writes a day, about one and a half million a second. That's a lot, but it's small writes to a cache.

Can we afford an inbox per user? Store only a post ID and a score, sixteen bytes. Five hundred entries for a billion users is eight terabytes. At sixty-four gigabytes per node that's about a hundred twenty-five nodes, two hundred fifty with a replica. Entirely reasonable. The posts themselves are about a kilobyte each, five hundred gigabytes a day, which goes in a durable store, not memory.

Traffic
DAU                1B
feed loads/day     10 each
  10B / 100,000 s = 100K rps
  peak x2         = 200K rps

posts/day          0.5 per user
  500M / 100,000  = 5K wps

avg audience       300
  inbox writes    = 150B/day
                  = 1.5M/s
Memory for inboxes
entry = postId 8 B + score 8 B
keep 500 newest per user
1B x 500 x 16 B  = 8 TB

8 TB / 64 GB per node
  = 125 nodes, x2 replicas
  = ~250 cache nodes

posts: 500M x 1 KB
  = 500 GB/day, ~180 TB/yr

Define API

The API is small. Publishing a post takes text, media references and an audience setting, like friends only or public. Reading takes a cursor and a limit and returns a page of fully rendered items plus the next cursor. A lightweight endpoint returns how many new posts arrived, for the 'new posts' pill. And the client reports which posts were actually seen, which feeds both deduplication and ranking.

Core API
POST /v1/posts
  { text, mediaRefs[], audience }
  -> { postId }
GET  /v1/feed?cursor=..&limit=10
  -> { items[], nextCursor }
GET  /v1/feed/new?since=..
  -> { count }  "new posts" pill
POST /v1/feed/seen
  { postIds[] }  impressions

Data model

Data model. Posts are written once and read by ID many times, so a key-value or wide-column store keyed by post ID fits. The social graph is a set of typed edges: friend, follows page, member of group. The inbox is a sorted set per user of post ID and score, capped at five hundred, and it's purely derived, so losing it costs speed, not data. Seen-state per user is a compact bloom filter.

Records
post   (postId, authorId, ts,
        audience, body, media)
       key-value by postId
edge   (srcId, dstId, type, ts)
       friend, follow, member
inbox  userId -> sorted set
       (postId, score), cap 500
seen   userId -> bloom filter
counts postId -> likes, shares

High-level design

The write path. The post service saves the post durably and only then publishes a post-created event to the stream. The fan-out service consumes it, asks the social graph for the audience, and adds the post ID to each audience member's inbox. Because the event is in a durable log, a crashed fan-out worker just resumes from its last offset.

The read path. The feed service pulls a few hundred candidate IDs from your inbox, plus posts from large pages pulled live. It sends them to the ranking service, which fetches features like how often you interact with each author, and returns scores. The feed service drops anything already seen or not visible to you, hydrates the top ten from the post store, and returns them with a cursor.

Deep dive: fan-out

Deep dive one: fan-out. Friend lists are usually capped at a few thousand, so friend posts are always safe to push. Groups and small pages are fine too. Pages with millions of followers are the celebrity case: don't push. Instead keep a short recent-posts list per big author, cached hot, and merge it in at read time. Also skip pushing to users who haven't logged in for a month; rebuild their inbox when they return.

Making fan-out safe. Partition the stream by author ID so one author's events stay in order. Adding to an inbox is idempotent, because a sorted set ignores a duplicate member, so retries can't double-insert. Deletes and audience changes publish an update event, but we never rely on that alone: the read path re-checks visibility, because a stale inbox entry must never leak a deleted or private post.

Who gets pushed
  • friend posts: audience capped
  • (a few thousand max) -> push
  • small pages, groups -> push
  • big pages (millions):
  • no push; keep a per-author
  • recent-posts list, pull and
  • merge at read time
  • idle 30+ days: skip push
Making fan-out safe
  • partition stream by authorId
  • -> one author in order
  • inbox add is idempotent:
  • same postId twice = no-op
  • delete or audience change:
  • publish post.updated, and
  • re-check at read time
  • backpressure: fan-out lag is
  • a metric, autoscale workers

Deep dive: ranking

Deep dive two: ranking. Think of it as a funnel. Start with about five hundred candidates. Filter out anything seen, blocked or not visible. A cheap model trims to a hundred, then a heavier model predicts how likely you are to like, comment or share each one, and combines those into one score. Finally, rules add diversity, like at most two posts from the same author in a row. All of this fits a budget of roughly a hundred milliseconds.

Where do features come from? Most are precomputed: how often you interact with this author, the post's age, how fast it's collecting reactions, which content types you engage with. They live in a key-value feature store updated by batch jobs and streams. Put a timeout on ranking. If it trips, fall back to recency order. And cache the ranked list for about a minute so scrolling to page two is consistent and cheap.

Ranking funnel
  • 1 candidates ~500 from inbox
  • + big pages
  • 2 filter seen, blocked,
  • not visible
  • 3 light model ~500 -> 100
  • 4 heavy model 100 -> scored
  • 5 rules diversity: max
  • 2 per author
  • budget ~100 ms for all of it
Features and fallbacks
  • features (precomputed):
  • viewer-author affinity
  • post age, early engagement
  • content type preference
  • feature store: key-value,
  • batch + streaming updates
  • ranking timeout 120 ms:
  • fall back to recency order
  • cache ranked page ~1 min

Deep dive: freshness

Deep dive three: freshness. The client reports which posts were actually on screen, batched every few seconds. A bloom filter per user remembers them in very little space; a false positive only means one post gets skipped, which is acceptable. For the new-posts pill, count inbox entries newer than the session start. A cheap poll every minute is enough; you don't need a WebSocket for a feed.

Seen and new
  • client batches impressions
  • every few seconds
  • seen set: bloom filter,
  • rare false positive only
  • hides one post
  • new-posts pill: count inbox
  • entries newer than session
  • start; poll every 60 s or
  • piggyback on other calls

Trade-offs

Trade-offs and failure modes. Push spends memory and write throughput to buy cheap reads; pull spends read latency to save writes; we mix both by audience size. An inbox shard can be lost and rebuilt from the graph and post store. Ranking can time out and fall back. Monitor fan-out lag, p ninety-nine page latency, ranking timeouts, and engagement metrics like click-through rate, because a ranking bug shows up there first.

At ten times the scale, lower the threshold so fewer authors are pushed, shard inboxes by user ID with consistent hashing, and run feed stacks per region with posts replicated asynchronously between them. Use a cheaper first-stage model and trim inboxes. And state the big trade: we gave up storage and a few seconds of freshness in exchange for fast reads and high availability.

What breaks first
  • hot author: graph lookup and
  • fan-out spike -> pull mode
  • inbox shard lost: rebuild
  • from graph + post store
  • ranking slow: recency fallback
  • privacy bug: always filter at
  • read, not only at write
  • monitor: fan-out lag, p99
  • page, rank timeouts, CTR
At 10x
  • lower the push threshold
  • shard inboxes by userId,
  • consistent hashing
  • regional feed stacks; replicate
  • posts across regions async
  • cheaper first-stage model
  • trim inboxes to 300 entries
  • traded: storage + freshness
  • for read speed + uptime

Say it like this

The summary. Posts are stored durably, then an event drives fan-out that pushes post IDs into per-user inboxes for friends and small pages, while big pages are pulled at read time. A feed request takes candidates from the inbox, filters seen and invisible posts, ranks them in two stages with a diversity pass, then hydrates the top results. Privacy is enforced at read time, and recency is the fallback.

The 60-second answer
  • post -> durable store -> event
  • fan-out pushes IDs to inboxes
  • (friends, small pages)
  • big pages pulled at read
  • read: candidates -> filter ->
  • light model -> heavy model
  • -> diversity -> hydrate
  • privacy checked at read
  • fallback: recency if slow

Interview questions

How is a news feed different from a simple timeline?

A timeline is one author’s posts in time order, while a news feed merges many sources and ranks them by predicted relevance. That adds candidate generation, a ranking stage and filtering on top of fan-out.

How much memory would per-user inboxes take?

If each entry is a sixteen-byte post ID and score and we keep five hundred per user, a billion users need about eight terabytes. That is roughly a hundred twenty-five sixty-four-gigabyte cache nodes before replication.

How do you rank the feed within a latency budget?

Use a funnel: generate a few hundred candidates, filter cheaply, score with a light model to cut to about a hundred, then score those with a heavier model and apply diversity rules. Put a timeout on ranking and fall back to recency order if it trips.

How do you make sure deleted or private posts never appear?

Treat the inbox as a hint, not the truth, and re-check each post’s existence and audience against the viewer at read time. Delete and audience-change events also clean inboxes, but the read-time check is the guarantee.

Why push friend posts but pull posts from big pages?

Friend audiences are capped at a few thousand, so pushing is bounded and makes reads cheap. Pages can have tens of millions of followers, so pushing would create huge write spikes and lag, while pulling their small recent-posts list at read time is cheap.

How do you avoid showing the same post twice?

The client reports impressions, and the server keeps a per-user seen set, often a bloom filter, that the feed service checks while filtering candidates. A cursor over a cached ranked page also keeps pagination stable.

Do you need WebSockets for new posts?

Usually not. A cheap periodic call that returns the count of inbox entries newer than the session start is enough to show a new-posts pill, and it costs far less than holding a billion connections.

What do you do if the ranking service goes down?

The feed service catches the timeout and serves candidates in reverse time order after the usual visibility filters. Users get a slightly less relevant feed instead of an error.