Design a social news feed
A home feed of recent posts from the accounts you follow, served in well under 200 ms. The core decision is when to copy a post into followers' feeds: when it is written, or when the feed is read. The hard case is an account with millions of followers.
Design the home feed for a social app with 50 million daily users. People follow each other, post text and photos, and open the app to see recent posts from the accounts they follow. A few creators have tens of millions of followers.
Requirements
Functional
- A user can publish a post (text plus up to four images).
- A user can follow and unfollow other accounts.
- A user sees a home feed of posts from accounts they follow, newest (or lightly ranked) first, paginated.
- A user sees their own new post in their feed immediately.
Non-functional
- Feed load p99 under 200 ms on the server (p99: 99 out of 100 requests are faster than this). The first page is 20 posts.
- A new post normally reaches followers' feeds within about 5 seconds. Eventual consistency is fine: feeds may lag a little but catch up.
- Feed reads are 99.95% available. A slightly old feed is better than an error.
- Scale: 50 million daily users, about 10 feed opens each a day. Reads outnumber posts by roughly 100 to 1.
Out of scope
- The ML ranking model itself (we leave a hook for it).
- Likes, comments and their counters; DMs; ads.
- Content moderation and spam detection.
Estimate
- Feed reads: 50M users x 10 opens = 500M a day; 500M / ~86,400 s = ~5,800 reads/s average; evening peak ~3x = ~17,000 reads/s.
- Posts: say 10% of users post once a day = 5M posts a day = ~60 posts/s average, ~200/s at peak.
- Fan-out on write: 5M posts x 200 followers on average = 1B feed inserts a day = ~12,000 inserts/s average, ~35,000/s at peak. Heavy but spreadable.
- One creator with 20M followers: 20M inserts for one post. At 50,000 inserts/s that is 400 s, almost 7 minutes, and it starves everyone else. This is why celebrities need different handling.
- Feed cache: 500 post ids x 8 bytes = 4 KB per user of raw data; 50M users x 4 KB = 200 GB, about 400 GB with a replica: a modest in-memory cluster. (A sorted set adds per-entry overhead that can multiply this several times, which is another reason to cache only active users.)
- Post metadata: 5M x 1 KB = 5 GB a day, ~1.8 TB a year. Images: say 30% of posts carry one 200 KB image = 1.5M x 200 KB = 300 GB a day, to object storage behind a CDN.
API
- POST /posts {text, mediaIds[]} -> 201 {postId}
- POST /media/upload-url {contentType} -> 200 {uploadUrl, mediaId}
- PUT /users/{id}/follow -> 204
- DELETE /users/{id}/follow -> 204
- GET /feed?cursor=&limit=20 -> 200 {posts[], nextCursor}
Data model
- posts(post_id PK time-sortable, author_id, text, media_ids[], created_at): wide-column store, also partitioned by author_id for author timelines
- follows(follower_id, followee_id, created_at) PK (follower_id, followee_id), plus a reverse table followers(followee_id, follower_id) for fan-out
- users(user_id PK, name, follower_count, is_celebrity) where is_celebrity = follower_count above ~100K
- feed:{user_id}: cache sorted set of post_id scored by time, capped at 500 entries
A request, step by step
- Posting: the app uploads images straight to object storage using a pre-signed URL (a short-lived link that allows one upload). Then it calls POST /posts. The post service saves the post, publishes a PostCreated event to the stream and returns 201 (Created). The post id is added to the author's own feed at once, so they see their post immediately.
- Fan-out (copying the post id into each follower's feed): a worker reads the event and checks the author. For a normal author, it pages through their followers using the reverse follow table. It adds the post id to each follower's feed, a sorted set trimmed to 500 entries. For a celebrity (above about 100K followers) it does nothing. Those posts are pulled when the feed is read.
- Reading: GET /feed goes to the feed service. It reads the user's cached list of post ids and fetches the latest posts of the few celebrities the user follows. It merges the two lists by time. Then it hydrates the top 20 ids: it fetches the full posts from the post store in one batch, with a cache of popular posts in front.
- Pagination uses a cursor (the last post_id seen), not page numbers. New posts arriving at the top then do not shift items or show them twice.
- We only fan out to recently active users. So a user who has not opened the app for weeks has no cached feed. On their first open, the feed service rebuilds it from the accounts they follow, then caches it.
Deep dives
Fan-out on write, on read, or both
Weak: pure fan-out on read. Each time a user opens the feed, query the posts of all 300 accounts they follow and merge them. At about 17,000 reads per second, that is millions of small queries a second.
The p99 latency (the time within which 99 out of 100 requests finish) explodes. One slow partition delays the whole merge.
Good: pure fan-out on write. Build each follower's feed when a post is created, so a read is one cache lookup. Reads become cheap and predictable.
The cost moves to writes. About 35,000 inserts per second at peak is fine. But one creator with 20 million followers produces 20 million inserts, and everyone queued behind them waits minutes. It also wastes work on followers who never open the app.
Great: a hybrid. Fan out on write for ordinary authors, and only to followers active in the last 30 days or so. Skip celebrities. Pull their recent posts when the feed is read instead. A user follows only a few celebrities, and those posts are already in cache because so many people read them. Inactive users get their feed rebuilt when they next open the app.
Trade-off: the hybrid adds a merge step and a threshold you must tune. In return you get bounded write amplification (a cap on how many writes one post can cause) and fast reads. Say the threshold out loud (about 100K followers) and explain how you would move it.
Celebrity posts and hot keys
When a cricket star posts, millions of feed reads ask for the same post within seconds. Even if we pull celebrity posts at read time, this is a hot-key problem: one key gets far more traffic than any other.
Weak: read the celebrity's post from the post store on every feed load. One partition gets hammered and its replicas fall over. Unrelated authors stored on that partition go down with them.
Good: cache each celebrity's recent posts in the feed cache, say the last 20 ids plus the post bodies. Give them a short TTL (time to live: the cache deletes the entry after that time). Feed reads hit memory, and only a cache miss touches the store.
Add request coalescing: when many requests miss the same key at once, only one goes to the store and the rest wait for its answer.
Great: also copy that hot key to several places. Keep copies of the celebrity timeline on several cache shards, or in a small in-process cache inside each feed server for a few seconds. Then no single cache node serves the whole country.
Also precompute each user's list of "celebrities I follow", so a feed read does at most a few lookups.
Trade-off: a celebrity post can be a few seconds out of date, and you use some extra memory. In return, the store is protected from the biggest spikes. Edits and deletes need an explicit invalidation event that removes the old copy. Waiting for the TTL to expire is not enough.
What the feed cache holds, and what happens when it is lost
Weak: store the full post in each follower's feed. A 1 KB post copied into 200 feeds turns 5 GB of posts a day into a terabyte of cache. An edit also has to rewrite every copy.
Good: store only post ids (8 bytes each), capped at 500 per user. When the user opens the feed, hydrate the 20 posts you show: fetch the full posts from the post store in one batched request.
Edits and deletes then show up on their own, because the post is read fresh. Deleted ids are filtered out during hydration.
Great: treat the feed cache as something you can rebuild, never as the source of truth. If a cache shard is lost, the feed service falls back to fan-out on read for those users. That is slower but correct.
Feeds are rebuilt lazily, when each user next opens the app. The fan-out stream can also be replayed from a recent point. Shard the cache by user_id with replicas. Limit how many rebuilds run at once, so losing the cache does not overload the database.
Trade-off: hydration adds an extra network call to every read. That is why the post store needs its own cache of popular posts. You give up a little read latency and get a cache that is small, cheap and safe to lose.
What breaks
- Fan-out lag grows, after a burst of posts or an account slightly below the celebrity threshold. Posts arrive late in feeds. Alert on consumer lag of the post-events stream (how far the workers are behind). The feed still loads, only with older posts.
- A feed cache shard fails: those users fall back to fan-out on read, with a limit on how many rebuilds run at once. Their p99 latency rises, but nothing is lost.
- The post store is slow and hydration times out. Return the posts that arrived in time and skip the rest, instead of failing the whole page.
- A post is deleted for policy reasons. An invalidation event removes it from caches. Hydration also filters deleted ids, so the post never shows even if its id stays in some feeds.
What each level is expected to cover
Mid level: Reaches fan-out on write with a feed cache of post ids. Sketches the API with cursor pagination. Puts media in object storage behind a CDN. Sees that a celebrity post is a problem when the interviewer asks about it.
Senior: Uses numbers to compare fan-out on write and on read, and proposes the hybrid with a stated threshold. Handles hot keys, inactive users and cache loss. Uses a stream for fan-out, with lag as the key metric. Explains how ranking would fit in without changing the storage design.
Common mistakes
- Choosing fan-out on write without doing the celebrity arithmetic (20 million inserts for one post).
- Storing full post bodies in every follower's feed instead of ids.
- Offset pagination (page 1, page 2) on a feed that changes every second, which shows some posts twice and skips others.
- Fanning out inside the POST request itself, so posting gets slower as an author gains followers.
- Treating the feed cache as the source of truth, with no way to rebuild it.
Follow-up questions
- How would you add ranking (not only newest first) without rewriting the storage layer?
- An account goes from 90K to 2 million followers overnight. What changes, and when?
- How do you handle unfollow: should old posts disappear from the feed?
- How would you show "new posts available" without the app asking the server every few seconds?
- What metrics tell you the feed is healthy?