Social & Feed
Design News Feed
01
Scope and quality bar
Scope and quality bar
- Publish a post containing text, up to four photos, or a link preview.
- Follow and unfollow accounts; the follow is one-directional and needs no approval for public profiles.
- Open a home timeline built from followed accounts, ordered by a relevance model rather than strictly by time.
- Scroll indefinitely with stable paging, so the same post never appears twice in one session.
- React, comment and reshare, with visible counts on each post.
- Threaded comment replies, two levels deep in the default view.
- Optional alerts when a close friend or a starred account publishes.
- First page of the home timeline returns in p99 under 250 ms from the API edge.
- The read path stays available at 99.95%; publishing may queue during an incident but must not lose a post.
- A new post shows up in most followers' timelines within 5 seconds; minutes are tolerable for the largest accounts.
- Accounts with tens of millions of followers must not create write storms or hot partitions.
- Deleted posts and blocked authors disappear from every timeline within a minute.
- Reaction counts may lag by a few seconds and may be approximate in the UI.
Pin down how skewed the follower distribution is, whether the feed is ranked or chronological, and how fresh a post has to be. Those three answers decide whether you can precompute timelines at all. Also ask whether the feed mixes in content from accounts the user does not follow, such as recommendations or ads. If it does, the candidate set is no longer just the follow graph, and you should say you will treat injected content as a separate source that joins at the ranking stage.
02
Back of the envelope
Back of the envelope
Our assumptions: 320 million people use the app daily, each follows about 220 accounts, and the average user publishes something roughly every two and a half days. A typical person opens the app eight times a day and pulls about one and a half pages of 20 posts per session.
| Quantity | Working | Result |
|---|---|---|
| Posts published per day | 320M x 0.4 | about 128M |
| Publish rate | 128M / 86,400 s, peak 3x | 1,480 / s, peak about 4,400 / s |
| Timeline page requests per day | 320M x 8 opens x 1.5 pages | about 3.84B |
| Timeline read rate | 3.84B / 86,400 s, peak 2.5x | 44,400 / s, peak about 111,000 / s |
| Read to write ratio | 3.84B pages / 128M posts, rounded down for retries | about 25 : 1 |
| Timeline inserts if every post is pushed | 128M x 220 average followers | about 28B per day, 326,000 / s |
| Timeline entry size | 8 B post id + 4 B author id + 4 B time + flags | about 20 B packed |
| Timeline cache, packed | 320M users x 500 entries x 20 B | about 3.2 TB, 9.6 TB with 3 replicas |
| Same cache as Redis sorted sets | about 64 B per member with overhead | about 10 TB before replication |
| Post metadata per day | 128M x about 1.2 KB | about 154 GB, 56 TB a year |
| Photo uploads per day | 22% of posts x 1.6 photos x 450 KB across renditions | about 20 TB into object storage |
| Reactions per day | 320M x 30 | 9.6B, about 111,000 / s |
Pushing every post to every follower means 326,000 timeline writes per second on average, and the average hides the tail. One account with 40 million followers posting five times an hour adds another 55,000 writes per second on its own. The design has to stop treating that account like everyone else.
The raw post rate is only a few thousand per second, and post bodies are small. Storing posts is easy; delivering pointers to them is the job. Reactions are the other heavy write stream, and they never need to be exact at read time.
03
API design
API design
Two choices in this API carry weight. Publishing returns 202 because the post is safe once the post store acknowledges it, and the fan-out after that is background work the client should not wait on. The timeline uses an opaque cursor that encodes a ranking snapshot, not a page number. Ranked feeds reorder constantly, and an offset into a list that keeps changing is how users end up seeing the same post on page one and page two.
04
How the pieces fit
How the pieces fit
flowchart LR
C["Mobile or web client"] --> GW["API gateway"]
GW --> PS["Post service"]
PS --> PDB[("Post store, sharded by post id")]
PS -. "PostCreated" .-> K[["Kafka: posts"]]
K --> FO["Fan-out workers"]
FO --> G[("Follow graph service")]
FO --> TL[("Timeline cache")]
GW --> FS["Feed service"]
FS --> TL
FS --> HA["Heavy author recent-post cache"]
FS --> RK["Ranking service"]
FS --> HY["Hydration: posts, profiles, counts"]
HY --> PDBThere are two halves that only meet in the timeline cache. On the write side, the post service validates the post, assigns a time-sortable 64-bit id, writes the row and emits an event. Fan-out workers consume that event, look up who follows the author, and append the post id to each follower's cached timeline. Nothing on this side knows about ranking.
On the read side, the feed service assembles candidates from two places: the follower's own precomputed timeline, and the recent posts of any very large accounts they follow, which were deliberately never pushed. It merges those lists, drops anything already seen or no longer visible, asks the ranking service to score the survivors, and then hydrates the top results with post bodies, author profiles and live counts. The result is cached as a short-lived snapshot so the next page comes from the same ordering.
The source of truth for posts, sharded by post id in a wide-column or sharded MySQL setup. Reads are by id lists, so a post cache in front of it absorbs most hydration traffic.
Two adjacency tables, followers of X and accounts X follows, each partitioned by the first user id. Fan-out reads the first; the feed service reads the second to find heavy authors.
One bounded list per active user holding the latest 500 entries. It is a derived cache, not a source of truth: anything in it can be rebuilt from the graph and the post store.
A stateless model server that takes a few hundred candidates plus viewer features and returns scores. It sits behind a strict deadline so a slow model degrades ordering, not availability.
Reactions and comments land in a log first and are folded into per-post counters every few seconds. The feed reads counts from a cache, never by counting rows.
Photos go straight from the client to object storage and are resized into three renditions asynchronously, then served through a CDN. The feed only ever carries media ids.
05
Deep dive: who pays for delivery
Deep dive: who pays for delivery
Assembling a feed costs work. You can pay it once per post, at publish time, by copying the post id into every follower's list. Or you can pay it once per read, by collecting posts from everyone the viewer follows when they open the app. With 25 reads per write you would think the answer is obvious, until you meet an account with 40 million followers.
Option one: build the feed when it is read
Pull is the simplest model. On each request, fetch the 220 accounts the viewer follows, read the latest few posts from each author's recent-post list, merge by time and rank. Writes are trivial, and nothing is wasted on followers who never open the app. The cost is on the hot path: 220 lookups per page, at 111,000 pages per second at peak, is about 24 million cache reads per second, and the slowest of those 220 decides your latency. Pull is fine for a small network and terrible as the only strategy at our size.
Option two: build the feed when it is written
Push flips the cost. When a post is created, workers write its id into the timeline of every follower, so reading a feed becomes a single range read of a precomputed list. That is exactly the shape a latency budget likes. The weakness is the long tail of follower counts. A single post from an account with 40 million followers turns into 40 million writes, which at a generous 500,000 writes per second for the whole fleet would take 80 seconds and block everyone else's posts behind it. It also burns memory writing into the timelines of people who have not opened the app in months.
What we do: push by default, pull for heavy authors
Split authors by follower count. Anyone under a threshold, say 75,000 followers, is pushed. Anyone above it is marked as a heavy author, and their posts go only into a small recent-post list keyed by author. At read time, the feed service takes the viewer's pushed timeline and merges in the recent posts of the heavy authors they follow. Most people follow only a handful of such accounts, so the pull side is maybe 5 to 15 extra lookups, all against a tiny, extremely hot cache that replicates well.
Two refinements make this work in practice. First, skip inactive followers during push. If a follower has not opened the app in 21 days, do not write to their timeline at all. When they come back, rebuild their timeline once by pulling from the accounts they follow. That alone removes a large share of push writes, because the follower lists of popular accounts are full of dormant users. Second, make the threshold sticky. An account hovering around 75,000 followers should not flip modes on every follow, so promote at 80,000 and demote at 60,000.
flowchart TD
R["GET /timeline/home"] --> SNAP{"Snapshot cursor valid?"}
SNAP -->|yes| PAGE["Serve next slice of snapshot"]
SNAP -->|no| P1["Read pushed timeline, 500 ids"]
SNAP -->|no| P2["Read recent posts of followed heavy authors"]
P1 --> M["Merge and dedupe"]
P2 --> M
M --> F["Filter: seen, deleted, blocked, muted"]
F --> L["Light ranker trims to 300"]
L --> H["Heavy ranker scores 300"]
H --> S["Store snapshot for 10 minutes"]
S --> PAGERanking in two passes
A candidate set of 500 to 800 posts is too many for a large model inside a 250 ms budget, and too few to need a search engine. So rank in two passes. A light ranker, a small logistic or tree model on cheap features such as author affinity, post age and media type, trims the set to about 300 in a few milliseconds. A heavy ranker, a neural model that also uses the viewer's recent interactions and the post's early engagement, scores those 300 in batch on CPU or GPU within 40 to 60 ms. A final pass applies business rules: no more than two posts in a row from one author, spread out reshares of the same item, and insert ads or suggestions at fixed slots.
The output is written as a snapshot keyed by viewer and a short id, holding the ordered list of post ids. The cursor carries the snapshot id and an offset. Scrolling reads the next slice of the same list, so ordering stays stable. Pulling to refresh discards the snapshot and builds a new one, and posts the viewer already saw are filtered out using a small per-user seen set.
No wasted writes, perfectly fresh, simple to reason about. Read cost grows with how many accounts a viewer follows, and the slowest shard sets the latency.
Too slow at this read rateFastest possible read. Write amplification is unbounded for large accounts, and dormant followers soak up memory and worker time.
Good until the first celebrityKeeps reads to one range scan plus a handful of hot lookups, and caps the cost of any single post. You pay with a merge step and a threshold to tune.
Bounded on both sides06
Choices we made and their price
Choices we made and their price
An entry is about 20 bytes, so 500 entries per user fit in a few terabytes. Edits and deletes happen once in the post store and show up everywhere on the next read. The price is a multi-get against the post cache on every page.
Small timelines, single source of truthSaves the hydration hop, but multiplies storage by roughly 60 and turns every edit or takedown into a second fan-out. Moderation would chase copies across millions of lists.
Storage and correctness costThe publish call ends once the post row and the event are stored. Workers can lag, retry and scale independently, and a backlog delays delivery without failing writes. Every timeline write is an idempotent set-add keyed by post id, so replays are harmless.
Publish latency independent of follower countWorks when the largest account has a few thousand followers. Beyond that the request times out, and a crash halfway leaves some followers with the post and some without, with nothing to resume from.
Breaks on big accountsEach reaction is a row keyed by post and user, which makes it idempotent and lets you answer "did I react". A stream job folds those into counters every two seconds, and hot posts get their counter split across 16 sub-keys that are summed at read.
Correct per user, cheap per postSimple, but a viral post sends tens of thousands of increments per second to one key on one shard. You also lose the ability to undo a reaction cleanly without the per-user row.
Fine for small appsFetching a thread is one query on root id, sorted by score or time, and the client builds the two visible levels. Deeper nesting is loaded on demand from the parent id. Easy to shard by root post.
Fits a two-level UIPath prefixes such as 0007.0012.0003 make whole-subtree reads one range scan, which helps forums with deep threads. For a feed where almost nobody expands beyond two levels, the extra write complexity buys little.
07
When things break
When things break
For each incident, say what the viewer notices, whether anything is lost, and what brings the system back. In a feed, the best answer is usually to serve something slightly worse rather than nothing: an older timeline, a chronological order, counts that are a minute stale.
One of the cache shards dies with its replica during a zone outage. Several million users now have no precomputed timeline, and their next request finds an empty list.
Treat an empty or missing timeline as a cache miss, not an empty feed: rebuild by pulling the latest posts from the accounts the viewer follows, cap the rebuild to the 200 most-engaged accounts, and rate-limit rebuilds per shard so recovery does not hammer the post store.A major live event triples the publish rate for 20 minutes. Workers cannot keep up and consumer lag climbs to several minutes. Posts exist, but followers do not see them yet, and people assume the app is broken.
Autoscale workers on lag rather than CPU, partition the topic by author so one busy author cannot block a partition for long, and temporarily lower the heavy-author threshold so the largest accounts switch to pull until the backlog drains. Always show authors their own post immediately from the post store.A head of state posts during a crisis. Fifty million feed requests in ten minutes all read the same recent-post list and the same post body, and the shard that owns them saturates.
Replicate heavy-author lists to every cache node and keep a copy in each feed service process for a few seconds. Hot post bodies get the same treatment. A hot key served from local memory costs nothing.A new model version is three times slower than expected, or the GPU pool shrinks after a bad deploy. Scoring 300 candidates now takes 400 ms and the whole feed misses its latency budget.
Give ranking a hard 80 ms deadline. If it misses, return candidates ordered by the light ranker's score, or by time as a last resort. Watch the fallback rate as a key metric, and roll out models behind a percentage flag.A post is removed for policy reasons, but its id is still sitting in millions of cached timelines and in snapshots built a few minutes ago.
Never trust the timeline for visibility. Hydration checks a tombstone set and the viewer's block list on every page, so a removed post drops out on the next request even if its id is still cached. Snapshots are short-lived for the same reason.The profile service starts answering in two seconds. Feed service threads wait on it, connection pools fill, and soon the feed fails even for requests that only needed cached data.
Set per-dependency timeouts well inside the page budget, isolate each dependency in its own pool, and open a circuit breaker when error rates spike. With profiles down, render posts with a cached author name instead of failing the page.| Incident | What the viewer sees | Data lost | Way back |
|---|---|---|---|
| Cache shard lost | Slower first load, then normal | None | Rebuild by pull, rate-limited |
| Fan-out lag | New posts arrive late | None | Scale on lag, lower threshold |
| Hot heavy author | Possible slow pages | None | Replicate and cache locally |
| Slow ranker | Less relevant ordering | None | Deadline plus fallback order |
| Stale deleted post | Nothing, if filters work | None | Tombstone check at hydration |
| Slow dependency | Degraded cards, not errors | None | Timeouts, bulkheads, breakers |
08
Mistakes to avoid
Mistakes to avoid
A query that joins the follow table to the posts table and sorts by time works in a demo and collapses once a single viewer follows hundreds of active accounts spread across shards.
The ranking changes between requests, so offset 20 on the second call points at a different list than the first call did. Users see repeats and miss posts.
Scoring 800 candidates with a large model on each of 111,000 requests per second is a GPU bill nobody approves, and most of those candidates were never going to make the page.
09
Interview advice
Interview advice
Name the skew in the first five minutes. Say that follower counts follow a power law and that the design must handle both a user with 200 followers and one with 40 million. Interviewers are waiting for you to see the celebrity problem before they raise it.
Explain push and pull with numbers. Compute writes per second for pure push and reads per page for pure pull. When you then propose the hybrid, it reads as the obvious conclusion instead of a memorised answer.
Separate candidate generation from ranking. Draw them as two boxes. It shows you understand that where posts come from and how they are ordered are separate problems with different scaling limits.
Store ids, hydrate late. Interviewers often ask how a deleted post leaves a million timelines. If your timelines hold only ids and hydration checks visibility, the answer is one sentence.
Talk about dormant users. Skipping push for followers inactive for three weeks is a cheap, real optimisation, and it shows you have thought about where the write budget actually goes.
Have a pagination answer ready. Snapshots and cursors come up almost every time with a ranked feed. Explain why offsets fail and what pull-to-refresh does to the snapshot.
Keep counters out of the feed's critical path. Reactions are a write stream as heavy as the timeline itself. Show the log plus batched aggregation, and mention sharded counters for viral posts.
Describe graceful degradation. If ranking, profiles or counts are slow, what does the user get? Interviewers reward a design that falls back to chronological order instead of an error screen.
Do not draw the final system first. Start from one database and a pull query, find where it breaks with your own numbers, then add the timeline cache, the log and the ranker in that order.
10
From one database to a global feed
From one database to a global feed
The feed is a query: posts where the author is in my follow list, newest first, limit 20. With an index on author and time this is fast enough, and there is nothing to keep in sync.
Feed queries get slow for people who follow many active accounts. Add a post cache, a profile cache and read replicas, and cache each author's recent post ids so the feed query becomes a merge of cached lists.
Switch to push with background workers and a durable log. The first large accounts appear, so introduce the heavy-author pull path and stop writing to dormant followers. Ranking starts as a simple scoring function.
Posts, follows and reactions move to sharded stores. Ranking becomes a two-pass model service with a feature store, snapshots fix pagination, and counters move to log plus batch. The single primary database is retired.
Users are homed in a region with their timelines, and posts replicate between regions asynchronously. Cross-region follows add a few seconds of delay, which the freshness target already allows.
Watch and read
Further watching and reading
Try next
Free to read · better with landed
Whiteboard this with landed
landed runs it as a live system design round on the whiteboard and grades your trade-offs.