System designDeep dive11 min

Recommendation & feed ranking

A ranked feed picks a few dozen items out of millions in a fraction of a second. It does that as a funnel: cheap retrieval from several sources, a light ranker, a heavy model that predicts what you'll do with each item, a value score that weighs those predictions, and rules that keep the result varied and safe. The hard parts are outside the model: features that match between training and serving, and a feed that shapes its own training data.

1 · The problem

A feed has a fraction of a second to choose 30 posts out of millions

An outdoor app has 3 million hikers who post trail reports, photos and closure notices, about 400,000 posts a day. Ravi opens the app at a trailhead near Denver. The home feed should show posts from people Ravi follows, but also a closure notice for a nearby trail from someone Ravi has never heard of, and nothing from the account that posts 40 sunset photos a day.

A purely chronological feed of followed accounts (Fan-out on write vs read) misses the closure notice and lets the busiest posters crowd everyone else out. Ranking everything is the opposite extreme. With the last two weeks of posts eligible, and a full ranking model that costs, say, 0.2 ms of CPU per post, the arithmetic rules it out:

Approach

Posts scored

CPU per feed open

Full model on every eligible post

400,000 × 14 = 5.6 million

5.6M × 0.2 ms ≈ 1,120 s

Full model on a shortlist

300

300 × 0.2 ms = 60 ms

The design question is how to build that shortlist so it still contains the best posts, and then how to order it. The answer in every published large-scale design is a funnel of stages, each one looking at fewer items with a more expensive model.

2 · The funnel

Narrow the pool in stages, each with a heavier model than the last

Published designs share the same shape. YouTube's recommender narrows millions of videos to hundreds with candidate generation, then ranks those. Instagram's Explore runs retrieval, a lightweight first-stage ranker over thousands of candidates, a heavy second-stage ranker on the best 100, and a final re-ranking. Facebook's feed runs a light model to pick about 500 posts, a main scoring pass, and a final pass for rules such as content-type diversity. For Ravi's feed:

Candidate sourcesfollows, nearby, similarLight ranker≈ 1,500 → 300Heavy rankerpredicts actionsRe-rankingdiversity, rulesRavi's feedfirst 30 posts≈ 1,500 posts300 posts300 scoredordered page

From 5.6 million eligible posts to 30 on screen. Retrieval must be cheap and broad, the heavy ranker precise, and re-ranking applies what no per-post score can see, such as two posts by the same author in a row.

Each stage has a different job, and judging them by the same measure is a mistake. Retrieval is judged by recall: did the posts Ravi would have liked make it into the 1,500? A post it drops can never be shown, however good the later stages are. The rankers are judged by how well their order matches what Ravi actually does. The light ranker exists only to keep the heavy one affordable.

The candidate sources are deliberately different from each other. X's For You pipeline takes about half its posts from accounts the user follows and the rest from out-of-network sources such as a graph of who engaged with what. The usual sources: similar items, personal history, local versus distant items, popular or trending items (Count-Min Sketch & top-K), and the social graph.

3 · Retrieval

Retrieval finds plausible posts cheaply, mostly with embeddings

The broadest source is usually an embedding model. A two-tower model learns two networks, one turning a user's features into a vector and one turning a post's features into a vector, trained so that a user's vector lands close to the vectors of posts that user engaged with. Instagram computes post vectors offline, loads them into an approximate nearest-neighbor index (Vector search & ANN indexes), and at request time runs the user tower on the freshest user features to search that index. The request path for Ravi:

Sources run in parallel, so retrieval costs about as long as the slowest source. Features for both Ravi and the posts come from a store kept current by batch and streaming jobs.

Each source returns a score of its own, such as an embedding similarity, but those scores aren't used to rank. Scores from different sources aren't comparable, and a shortlist is small enough for one model with far richer features to score every candidate the same way. Where the query is known ahead of time, as in "posts similar to this one", the nearest neighbors can even be computed offline and stored.

4 · Ranking

Rank by predicted actions, combined into one value score

The heavy ranker doesn't predict "how good is this post". It predicts several concrete actions, each as a probability: will Ravi like it, save it, comment, or hide it. X's ranker predicts ten such actions, from a like to a report, using about 6,000 features per post. The predictions are combined into one value score as a weighted sum, the same formula Instagram and Facebook use to combine their predictions.

The weights are product decisions, and negative actions get large negative weights. In X's April 2023 configuration a predicted reply weighed 13.5, a predicted like 0.5, and a predicted report −369. Suppose the trail app weighs a like 1, a save 4, a comment 6 and a hide −40. Three posts for Ravi:

Closure notice nearby
0.76 score
Friend's summit photo
0.6 score
Viral waterfall reel
0.12 score

Score = 1 × P(like) + 4 × P(save) + 6 × P(comment) − 40 × P(hide). The reel has the highest like probability (0.45) but a 1% chance of being hidden costs it 0.40. The closure notice wins on saves: 0.08 + 0.60 + 0.12 − 0.04 = 0.76.

The objective shapes the feed. Ranking by click probability promotes click-bait that people don't finish, which is why YouTube's ranker predicted expected watch time instead; but pure watch time favors very long videos. Mixing several predicted actions, including negative ones, is how feeds balance these. Facebook's weights are calibrated against surveys asking people which posts were worth their time, because engagement alone doesn't say that.

5 · Re-ranking

Re-rank the top for variety, freshness and rules no score can see

Sorting by value score alone gives a poor page. If the sunset photographer's 40 posts all score well, they fill the screen. Some rules depend on the whole list, not one post, so they run after scoring. X's pipeline applies author diversity, a balance of followed and recommended posts, fatigue from earlier negative feedback, removal of posts already seen, and filtering of blocked and muted accounts. Instagram's final stage avoids showing items from the same author in sequence.

  1. Remove

    Posts Ravi has seen, blocked or muted authors, and anything flagged by safety checks.

  2. Spread authors

    At most one post per author in any five consecutive slots.

  3. Balance sources

    Keep followed accounts at roughly half the page, so recommendations can't crowd out friends.

  4. Boost fresh and local

    Lift reports under a day old about trails within 50 km of Ravi, because trail conditions go stale fast.

  5. Fill the page

    Take the first 30 and return a cursor for the next page.

Freshness also enters earlier. Users prefer fresh content, but not at the expense of relevance; YouTube feeds its model each training example's age so it can learn how popularity changes after upload, and retraining often and using a post's age as a feature help too. Finally, keep a fallback: X's For You timeline has a reverse-chronological pipeline to fall back on if the scored pipeline fails, so a ranking outage degrades the feed instead of emptying it.

6 · Features

Features must mean the same thing in training and in serving

A ranker is only as good as its features, and most are counts that change by the minute: how many saves a post has, how many trail reports Ravi opened today. Facebook notes its feed needs to know that an article got many likes even if it was posted minutes ago. Those counts come from streaming jobs over the event log (Stream processing), while slower features such as a user's long-term interests come from batch jobs; X's repository has one framework for aggregate features computed in either mode.

The classic failure is training-serving skew: the model performs worse in production than in training because the features differ. Code computing a feature one way in the training pipeline and another way at serving time causes it, and so does data changing between training and serving. The standard defenses:

Defense

What it prevents

Log the features used at serving time and train on those logs

Training on values the model never actually saw

Share feature code between training and serving

Two implementations drifting apart

Point-in-time joins for historical features

Leaking a feature value from after the event into training

Monitor feature distributions in both places

Skew going unnoticed

The point-in-time row deserves an example. If a model learns whether Ravi saved a post, the save count it sees must be the count at the moment the post was shown, not today's count, which already includes Ravi's own save. A feature store such as Feast stores features as time series and reproduces their value at each training example's timestamp.

7 · Feedback loops

The feed writes its own training data, so correct for position and test live

Every click the model learns from happened on a page the model arranged. Posts at the top get clicked more because they are at the top, and a model trained naively learns to rank them high again. That is position bias. Common fixes are to give the model the position as a feature during training and a fixed default when serving, keeping that feature separate from the rest; YouTube's ranker adds a small separate network that models the bias so the main model doesn't absorb it.

A second loop starves new posts. A model that ranks only by each post's own history can never learn that a new post deserves to be shown, because it never shows it. A new trail report needs exposure before there is any engagement to learn from, so reserve a few slots per page for new posts and new authors, and measure how they do.

Offline metrics guide development, but they aren't the verdict. Live A/B results are not always correlated with offline experiments, so every change ships as an experiment and is judged by live metrics, including negative ones such as hides and reports. The checklist for the trail app:

Decision

Choice for the trail app

Retrieval

Several sources in parallel: follows, nearby, two-tower ANN, trending

Ranking

Light ranker to 300, then a heavy model predicting each action

Value score

Weighted sum of predicted actions; large negative weights for hides

Re-ranking

Remove seen and blocked; spread authors; balance sources; boost fresh local reports

Fallback

Chronological feed of follows when ranking fails

Features

Streaming counts plus batch profiles; logged at serving; point-in-time training

Bias and newness

Position handled in training; reserved slots for new posts

Evaluation

Offline to iterate, live A/B tests to decide

Check yourself

4 questions

1. Why does a feed use a light ranker before the heavy model instead of running the heavy model on every candidate?
2. A post has the highest predicted like probability but a 1% chance of being hidden, and hides weigh −40. What happens in a value-score ranking?
3. A model trains on today's save counts for posts shown last month. What goes wrong?
4. Clicks on the top three slots are far higher than on slots further down. How should training account for that?

Takeaways

Remember this

  • Rank in a funnel: broad, cheap retrieval from several sources, a light ranker, then a heavy model on a shortlist.
  • Retrieval is judged by recall, because a post it drops can never be shown.
  • Predict concrete actions and combine them into a value score, with large negative weights for hides and reports.
  • Re-rank the top for author diversity, source balance and freshness, and keep a chronological fallback.
  • Log serving features, use point-in-time joins, correct for position bias, reserve slots for new posts, and decide by live A/B tests.