Courses › System Design › Question Breakdowns

Design a News Feed

Fan-out on write vs. read, the celebrity problem, time-sortable IDs and feed ranking.

Design the home feed of a social network like Facebook, X or LinkedIn: users follow others and see a stream of their posts. The central challenge is fan-out: one post must reach every follower's feed, and follower counts range from ten to tens of millions.

Requirements

Functional

  • Users can create posts
  • Users can follow other users
  • Users can view a feed of posts from people they follow, newest first, with pagination

Out of scope: likes and comments, ranking (we'll discuss it as an extension), media processing.

Non-functional

  • Low latency feed loads (< 200ms)
  • Eventual consistency is fine — a post can take a few seconds to appear in feeds
  • Scale: 500M daily active users; users follow up to ~2,000 accounts; some accounts have 50M+ followers
  • A new post should reach followers' feeds within ~5 seconds

[!TIP] Back-of-the-envelope 500M users loading their feed ~5 times a day is 2.5B feed reads/day (~30k/second), with peaks several times higher. If 100M posts/day are written, that's ~1,200 posts/second: reads outnumber writes ~25×. Keeping the latest ~500 post IDs per user (8 bytes each) is ~4 KB per user, ~2 TB for everyone, which fits in a Redis cluster. Caching full 1 KB posts instead would be ~250 TB, so cache IDs per user and post bodies once.

Core Entities

  • User, Post (author, text, created_at), Follow (follower, followee)

API

POST /v1/posts            { "text": "..." }    → { "postId" }
PUT  /v1/follows/{userId}                      → 204
GET  /v1/feed?cursor=&limit=20                 → { posts[], nextCursor }

The cursor is opaque to the client: it encodes the last post ID returned, which works because post IDs sort by time (see deep dive 8). Asking for "newer than the first item I have" uses the same mechanism in the other direction, for the "new posts" banner.

High-Level Design

The naive design — on each feed request, look up everyone I follow, fetch their recent posts and merge — works at small scale:

Client -> [Feed Service] -> (Follows DB)
[Feed Service] -> (Posts DB)

But with 2,000 followees it means thousands of lookups per feed load, repeated on every refresh. It won't meet 200ms.

The full architecture

a = client "User A\n(posts)" @0,1
web = service "API Gateway" @1,1.5
app = service "Post Service" @2,1
pdb = store "Posts DB" @3,1
media = blob "Photo / Video\nStorage" @2,0
q = queue "Fan-out Queue" @2,2
gen = service "Fan-out\nWorkers" @3,2
fc = cache "Feed Cache\n(post IDs per user)" @4,2
notify = service "Realtime\nGateway" @3,3
bc = client "Users B, C" @0,3
a -> web
bc -> web
web -> app
app -> pdb
app -> media
app -> q
q -> gen
gen -> fc
gen -> notify
notify --> bc : N new posts
  • The write path stores the post, then drops a fan-out job on a queue and returns; the author never waits for fan-out.
  • Fan-out workers expand each job into appends to follower feeds, in batches of a few thousand followers per job so one big account can't stall a worker.
  • The read path touches only the feed cache and the post cache, which is how it stays under 200ms.
  • A realtime gateway tells apps that are open that new posts exist; the app fetches them itself.

Deep Dives

1. Fan-out on write (precomputed feeds)

When a user posts, push the post ID into a precomputed feed for each follower (a Redis list or a Cassandra row per user, capped at the latest ~500 IDs). Reading a feed becomes a single lookup plus hydrating post content (from cache).

[Post Service] -> {Fan-out Queue} -> [Fan-out Workers] -> (Feed Cache per user)
Client -> [Feed Service] -> (Feed Cache per user)

Fan-out happens asynchronously in workers so posting stays fast.

What a feed entry holds: just (post_id, author_id), 16 bytes. A Redis sorted set scored by post ID makes "the next 20 older than cursor X" one ZREVRANGEBYSCORE call. Cap each feed at ~500 entries (ZREMRANGEBYRANK after each append); someone scrolling past 500 posts gets older pages from the fan-out-on-read path below.

Cost check: an average post reaches ~200 followers, so 100M posts/day becomes ~20B feed appends a day, ~230k/second. That's a large but ordinary Redis cluster, and it's the number that justifies everything in the next two sections.

2. The celebrity problem

Fan-out on write breaks for an account with 50M followers: one post means 50M writes.

Hybrid approach:

  • Accounts with fewer than a threshold of followers (say 100k) → fan-out on write
  • Celebrity accounts → no fan-out; their recent posts are kept in a small, heavily cached per-author list
  • On feed read, merge the precomputed feed with recent posts from the celebrities this user follows (usually only a handful)

3. Pagination

Use cursor-based pagination keyed by (created_at, post_id) so new posts arriving at the top don't shift pages and duplicate items. Offsets don't work for a constantly changing list.

4. Inactive users

Don't fan out to users who haven't logged in for weeks — it wastes most of the write work. Rebuild their feed on demand (fan-out on read) when they return.

5. Delivering new posts to an open app

Users don't want the feed to move under their thumb, so don't insert posts into the visible list. The app shows a "New posts" pill, and tapping it loads them.

The pill needs a signal. An app in the foreground holds an SSE or WebSocket connection to the realtime gateway, which sends only "N new posts", never the posts themselves; at most one such hint every 30 seconds per user keeps a busy follow list from flooding the connection. An app in the background gets nothing. It refreshes when the user opens it.

6. Storage

  • Posts: sharded by post ID (or author ID); cached heavily since popular posts are read millions of times.
  • Follows: stored both directions (follower → followees, followee → followers) for both fan-out and feed reads.
  • Feeds: Redis for hot users, with Cassandra as a durable backing store if needed.

7. Ranking (extension)

A ranked feed retrieves candidates (the precomputed feed + celebrity posts + recommendations), scores them with a model using engagement signals, and returns the top N. Mention it as a separate candidate generation → ranking stage.

A ranking model predicts, for each candidate, the probability of a few actions (like, reply, dwell over 5 seconds, hide) and combines them into one score. The candidate pool (a few hundred posts) and the model's features (the author's recent engagement, the viewer's history with that author, the post's age) are fetched in parallel, so ranking adds tens of milliseconds. Keep a fallback: if the ranker times out, serve the chronological feed.

8. Sharding posts, and IDs that sort by time

With precomputed feeds, the posts store only answers point lookups by post ID (hydration) plus "an author's recent posts" (profiles and the celebrity merge). So shard posts by author ID and cache aggressively: hydration is served almost entirely from the post cache, so a busy author's shard sees little load.

Post IDs should sort by time so cursors and sorted sets work without a separate timestamp. Twitter's published Snowflake layout is the standard answer:

| 1 unused bit | 41 bits: ms since a custom epoch | 10 bits: generator ID | 12 bits: sequence |

That's ~69 years of milliseconds, 1,024 generators and 4,096 IDs per millisecond per generator, with no coordination between generators. Clock skew is the one hazard: a generator must refuse to issue IDs if its clock jumps backward.

Feed cache sharding is simpler: partition by user ID with consistent hashing. One user's feed (a few hundred IDs) always fits on one node, so a feed read hits exactly one server.

9. The post cache

Hydrating a page means fetching 20 post bodies, so the post cache carries most of the read load: 30k feed loads/second × 20 posts is ~600k lookups/second at the average, several times that at peak. Use MGET to fetch a page in one round trip, and give each post a TTL of a few days, since feeds rarely show older posts. A viral post can overwhelm the one cache node that holds it, so keep a small in-process cache on each feed server for the hottest few thousand posts.

10. Losing the feed cache, and the metric to watch

The feed cache can always be rebuilt from the follow graph and the posts store, so treat it as disposable: when a node is lost, its users get fan-out-on-read until their feeds are rebuilt in the background. The metric that pages someone is fan-out queue lag. If it climbs past the ~5-second freshness target, add workers or raise the celebrity threshold so fewer posts fan out.

11. Features interviewers add

  • Reposts: a feed entry gets a third field, the reposter, and hydration shows "Ana reposted". Deduplicate when two accounts you follow repost the same post.
  • Muting and blocking: filter at read time, not at fan-out, so unmuting takes effect instantly.
  • Trending topics: a separate streaming count over recent posts (see Design Top-K (Trending)).

What's expected at each level

  • Mid-level: a working design and recognition that the naive read path is slow.
  • Senior: fan-out on write, the celebrity problem and a hybrid solution, cursor pagination.
  • Staff: cost trade-offs of fan-out, inactive users, ranking architecture, sharding with time-sortable IDs, and how the system evolves.