Design a News Feed
Design the home feed of a large social network. When someone opens the app, they see a scrolling list of recent posts from the people and pages they follow, ordered so the most interesting items come first.
The network has hundreds of millions of daily users. Most accounts have a few hundred followers, but some public figures have tens of millions, and a single post from one of them must not stall everyone else's posts. Users expect the feed to load almost instantly and to include a friend's post within seconds of it being shared.
In 45 minutes, agree on scope, estimate the load, and design both how posts reach feeds and how a feed is assembled and ranked when it is requested. Be ready to defend where you do the work: when a post is written, or when a feed is read.
Sketch on the HLD board, write your answer in the Design doc, and size it with the Estimator. Start the mock interview from the timer for a 45-minute run.
Design Path · Social & Messaging · problem 5 of 18
Ask before you design. Try answering each yourself, then compare.
- Is the feed strictly chronological, or ranked?
Typical answer
Ranked by a relevance score, but recency is a big part of it. A simple scoring model is fine; we will not design the ML training pipeline.
- What scale are we designing for?
Typical answer
About 300 million daily users, each opening the feed around 10 times a day and posting about once every two days.
- How many accounts does a typical user follow, and how skewed are follower counts?
Typical answer
Around 200 on average with a cap of 5,000 followees. Follower counts are heavily skewed: a few thousand accounts have more than a million followers.
- How fresh must the feed be?
Typical answer
A new post should show up for followers within about 5 seconds in the normal case. A minute is acceptable for accounts with huge audiences.
- Do posts include photos and video?
Typical answer
Yes, but media is uploaded to blob storage and served by a CDN; the feed only carries references to it.
- Do we need to support ads, edits and deletes?
Typical answer
Deletes and privacy changes must take effect quickly. Ads are out of scope; edits can be treated like deletes plus a new version of the content.
Requirements
Functional
- Users create posts (text plus media references) that appear in their followers' home feeds.
- A user loads their home feed as a ranked, paginated list and can scroll back through older items.
- Following, unfollowing, blocking and deleting a post are reflected in feeds.
Non-functional
- Feed load p99 under 200 ms for the first page; 99.95% availability for reads.
- New posts visible to followers within 5 s at p95 (up to 60 s for accounts with over 1 M followers).
- Handle 300 M DAU, around 100 K feed reads per second at peak.
- A deleted or privacy-restricted post must disappear from feeds within seconds.
Out of scope
- Training the ranking model and the ads system.
- Media upload and transcoding (assume a blob store and CDN exist).
- Search and trending topics.
Back-of-the-envelope estimates
- Posts: 300 M users × 0.5 posts ≈ 150 M posts/day ≈ 1,750/s, about 5,000/s at peak.
- Feed reads: 300 M × 10 opens ≈ 3 B/day ≈ 35,000/s, about 100,000/s at peak, so roughly 20 reads per post.
- Fan-out: 150 M posts × 200 followers ≈ 30 B feed inserts/day ≈ 350,000/s on average, around 1 M/s at peak. This is the number that shapes the design.
- Feed cache: keep the newest 500 entries per user at ~16 bytes each (post id plus a timestamp or score) ≈ 8 KB per user. For 300 M active users that is about 2.4 TB of RAM before replication: a large but ordinary Redis cluster.
- Post storage: 150 M × ~1 KB of text and metadata ≈ 150 GB/day, about 270 TB over 5 years and roughly 800 TB with 3 replicas. Media lives in blob storage and is far larger, but it is not on the feed path.
The fan-out rate and the cost of a single post from a mega-account drive the design; the read QPS is high but easy once feeds are precomputed.
API
POST /v1/posts {text, mediaIds[], audience} -> 201 {postId}
DELETE /v1/posts/{postId} -> 204
GET /v1/feed?cursor=&limit=20 -> {items:[{post, author, counts}], nextCursor}
POST /v1/follows {targetUserId} -> 204
DELETE /v1/follows/{targetUserId} -> 204The feed uses an opaque cursor that encodes the last-seen position and the ranking snapshot, not a page number, because new posts arrive constantly and offset paging would show duplicates or skip items as the list shifts.
Data model
posts(post_id PK, author_id, created_at, text, media_refs, visibility) in a wide-column store partitioned by post_id. Post ids are time-sortable 64-bit ids (timestamp plus shard plus sequence), so a feed entry can be sorted without fetching the post.
user_posts(author_id, post_id DESC) is a second table partitioned by author; it powers profile pages and the pull path for celebrity accounts.
follows(follower_id, followee_id, created_at) lives in the social graph service, stored twice (by follower and by followee) so both "who do I follow" and "who follows me" are single-partition reads.
Feed cache: one Redis sorted set per user, key feed:{user_id}, members are post ids and scores are timestamps, trimmed to the newest 500. It is a cache, not the source of truth: it can be rebuilt from follows plus user_posts. Engagement counters (likes, comments) live in a separate counter store so hot posts do not rewrite the post row.
High-level design
Write path. The client sends a post through the API gateway to the Post Service, which assigns a time-sortable id, writes the row to the Post Store and appends a post created event to the Post Events stream before returning. Fan-out Workers consume the stream. For each event they ask the Graph Service for the author's followers in pages of a few thousand, then add the post id to each follower's sorted set in the Feed Cache and trim it to 500 entries. Authors above a follower threshold (say 1 M) are skipped here; their posts are only indexed in the author's own recent-post list.
Read path. A feed request goes to the Feed Service. It reads the viewer's precomputed entries from the Feed Cache, then pulls the latest posts from the handful of large accounts the viewer follows out of the Post Cache. It merges the two lists, drops anything from blocked or unfollowed authors, and sends a few hundred candidates to the Ranking Service, which scores them using features such as author affinity, engagement and age. The Feed Service then hydrates the top 20 ids into full posts from the Post Cache and returns a page plus a cursor.
Writes are asynchronous after the post is saved, so posting stays fast, and reads touch only caches in the common case.
Open the HLD board and press Reference to see the architecture diagram.
Deep dives
Push, pull, or both
Pure fan-out on read means each feed request must query recent posts from hundreds of followees and merge them: easy writes, but expensive and slow reads at 100 K requests per second. Pure fan-out on write makes reads a single cache lookup, but a post by an account with 50 M followers becomes 50 M cache writes, delaying everyone else's posts behind it. The hybrid splits authors by audience size. Normal accounts are pushed; accounts above roughly 1 M followers are pulled at read time. Because a user follows at most a few dozen such accounts, the pull is a small, bounded merge of short lists that are themselves extremely hot and therefore always cached. The threshold can also depend on activity: pushing to followers who have not opened the app in 30 days is wasted work, so skip them and rebuild their feed on their next visit.
Ranking without blowing the latency budget
Ranking happens in two stages. Candidate generation gathers around 500 ids cheaply from the feed cache and celebrity lists. Scoring then runs a lightweight model over those candidates using precomputed features: viewer-to-author affinity from a feature store, engagement counts, post age and content type. The model call is batched per request and must fit inside about 50 ms. To keep pagination stable, the Feed Service stores the ranked list for the session for a few minutes and serves later pages from it, so scrolling down does not reshuffle items the user already saw. If the ranking service is slow, fall back to reverse chronological order rather than failing the request.
Deletes, unfollows and privacy changes
Feed entries are only ids, so a deleted post does not need to be chased out of millions of sorted sets. The post row is marked deleted, the post cache entry is invalidated, and hydration simply drops ids whose post is missing or hidden. Unfollows and blocks work the same way: the Feed Service filters candidates against the viewer's current follow and block lists, which are small and cached, and a background job lazily removes stale entries. This read-time filtering is what makes the 'disappears within seconds' requirement achievable without a massive cleanup fan-out.
Keeping fan-out workers healthy
Partition the post-events stream by author id so one author's posts are processed in order. Split each big fan-out into tasks of about 5,000 followers so many workers share the work, and give normal accounts a separate priority lane so a 900 K-follower post cannot delay ordinary posts. Cache writes are idempotent (adding an existing member to a sorted set changes nothing), so retries after a worker crash are safe. Pipeline Redis commands per shard to cut round trips; at 1 M inserts per second, batching is the difference between 50 and 500 cache nodes.
Failure modes
- If the Feed Cache shard for a user is lost, the Feed Service rebuilds that feed on demand from the follow list and authors' recent posts, slower but correct, while a background job warms it again.
- If fan-out workers fall behind (for example after a viral event), feeds are stale but still served; monitor consumer lag and add workers, and prioritise active followers.
- If the Ranking Service times out, return the candidates in reverse chronological order so the feed still loads.
- If the Graph Service is unavailable, pause fan-out (events stay in the stream) rather than dropping posts, and keep serving reads from the cache.
Core algorithm in code
import heapq
import math
def score(post, now):
"""Rank a candidate post: engagement boosted, decayed by age in hours."""
age_h = max(0.0, (now - post["ts"]) / 3600)
engagement = math.log1p(post["likes"] + 3 * post["comments"])
affinity = post.get("affinity", 1.0) # how often the viewer interacts with the author
return affinity * (1 + engagement) / (age_h + 2) ** 1.5
def build_feed(pushed, celeb_posts, now, limit=5):
"""Merge pre-computed (pushed) entries with posts pulled from followed celebrities,
drop duplicates, rank, and return the top `limit` post ids."""
seen, candidates = set(), []
for post in pushed + celeb_posts:
if post["id"] in seen:
continue
seen.add(post["id"])
candidates.append(post)
return heapq.nlargest(limit, candidates, key=lambda p: score(p, now))
NOW = 1_700_000_000
H = 3600
# Written into this viewer's feed cache at post time by the fan-out workers.
pushed = [
{"id": "p1", "author": "friend_a", "ts": NOW - 1 * H, "likes": 4, "comments": 1, "affinity": 2.0},
{"id": "p2", "author": "friend_b", "ts": NOW - 20 * H, "likes": 40, "comments": 9},
{"id": "p3", "author": "friend_c", "ts": NOW - 3 * H, "likes": 0, "comments": 0},
]
# Read at request time from the recent-post lists of the two celebrities the viewer follows.
celeb_posts = [
{"id": "c1", "author": "star_x", "ts": NOW - 2 * H, "likes": 90_000, "comments": 4_000, "affinity": 0.4},
{"id": "c2", "author": "star_y", "ts": NOW - 30 * H, "likes": 250_000, "comments": 12_000, "affinity": 0.4},
]
for p in build_feed(pushed, celeb_posts, NOW):
print(f"{p['id']:>3} by {p['author']:<9} score={score(p, NOW):.3f}")
Trade-offs
- Fan-out on write gives single-lookup reads but costs huge write amplification and wasted work for inactive followers.
- Fan-out on read keeps writes cheap and always fresh but costs many reads and a merge on every feed request.
- Storing only post ids in feeds keeps memory small and makes deletes cheap, but costs a hydration step on every read.
- Caching the ranked list per session gives stable pagination but costs freshness: posts arriving mid-session wait for the next refresh.
- A lower celebrity threshold reduces write spikes but pushes more merge work and latency onto reads.
What a strong answer shows
- Quantifies fan-out volume and uses it to choose a push/pull hybrid rather than asserting one.
- Handles high-follower accounts explicitly, with a threshold and a read-time merge.
- Stores ids rather than full posts in feeds and explains hydration, deletes and memory.
- Separates candidate generation from ranking and has a fallback when ranking is slow.
- Keeps the post request fast by making fan-out asynchronous and idempotent.
- Designs stable cursor-based pagination for a constantly changing list.
Mistakes to avoid
- Choosing pure fan-out on write and never noticing the celebrity problem.
- Copying whole post bodies into every follower's feed, which explodes storage and makes edits and deletes impossible.
- Running the fan-out synchronously inside the create-post request.
- Using offset pagination, so users see duplicates as new posts arrive.
- Spending the whole interview on the ranking model instead of the data flow.
Follow-ups to expect
- How would you add ads into the feed? (A separate ad service returns candidates; the feed service blends them in fixed slots after ranking.)
- How would you show 'new posts available' while the user is scrolling? (Poll a lightweight endpoint or push a signal over a long-lived connection; do not reorder the current list.)
- How do you handle a user who follows 5,000 accounts? (Cap the pull merge, rely on push, and let ranking pick from a bounded candidate set.)
- How would you run the feed in multiple regions? (Fan out in the author's region, replicate events, and build each user's feed in their home region.)