A Like Is Worth 0.5: How the X Algorithm Actually Ranks Your Feed

Recently, X published a major update to the code behind its For You feed. I went through the repository with AI, traced the main request path, ranking logic, filters, and configuration, and pulled out the architectural decisions I found most interesting.

 

x feed range
Literal constants in a config file. Each coefficient weights one action probability predicted by the ranking model.

The shape of a request

On a cold request, the system builds a candidate pool on demand, scores it, filters it, and returns the final feed. Nearby requests can reuse a short-lived candidate cache, which we'll get to later.

A few thousand candidate posts get gathered, 50 survive scoring, and 35 end up in your feed.

X For You feed request flow
One request, end to end. The fan-out at the top runs concurrently; the chain below it cannot.

Two moments in that diagram are what I want to highlight:

  1. Some stages run at the same time, some can't. Fetching candidates and enriching them happen in parallel; a dozen network calls fire simultaneously, and the results get merged. Filtering and scoring run strictly in sequence, because each step depends on the one before it. You can't dedupe against a list you haven't built yet.
  2. 50 posts are selected, but only 35 are shown. That 15-post gap is deliberate slack. Visibility checks happen after ranking, and some posts will get dropped. The extra 15 mean you don't end up with holes in your feed.

Where the posts come from

The request fans out across multiple candidate sources and queries them concurrently. Two paths are especially useful for understanding the architecture: Thunder for in-network posts and Phoenix for ML-based retrieval.

Where the posts come from for twitter
In-network and out-of-network candidates are merged into one pool and ranked by the same model.
  • Thunder keeps recent posts in memory as they're published and hands back the ones from accounts you follow.
  • Phoenix maps the viewer and candidate posts into learned representations, then retrieves posts that look promising for that viewer.

The viewer representation is not a single static profile vector. It combines learned user embeddings with recent interaction history.

That distinction matters. Recent behavior directly affects retrieval, but the model still contains a persistent per-user representation. So changing what you engage with should move the candidate pool, while the repository does not support the stronger claim that recent actions are the only information defining you.

Sixty predictions, one score

Here's the design decision the whole system hangs on: the ranking model doesn't predict "relevance." It predicts about sixty separate things, and combining them is a separate step.

For every post, it asks: how likely is this person to like it? Reply? Repost? Quote? Share it in a DM? Copy the link? Click through to the profile? Open the link? Watch the video? Follow the author? And the ones nobody wants: mark not-interested, mute, block, report? Plus continuous guesses, like how many seconds you'll spend looking at it.

Then those sixty probabilities get multiplied by sixty weights and summed.

X ranking model probabilities and weights
Probabilities in, explicit coefficients applied, one score out. The model learns the probabilities; the ranking layer combines them using configured weights.

The weights, as they currently ship:

X post feed range weight
X post feed range weight

A like is the cheapest signal there is

Look at the ordering. Among positive engagement terms, the like coefficient is one of the smallest. The reply coefficient is 10× the like coefficient. The copy-link coefficient is 40× larger.

That does not mean one observed reply equals ten observed likes. These coefficients multiply the model's predicted probability of each action.

One plausible product explanation is friction: a like takes one tap, while sharing a post requires a stronger action. The code gives another explicit reason: the weights also account for typical action frequency. Common actions need less amplification; rare actions need larger coefficients before their predicted probabilities materially affect the final score.

The 468 myth

Divide 234 by 0.5, and you get 468. That number went around a lot: "one report cancels out 468 likes."

It's wrong, and the code now carries a long comment explaining why.

The actual mechanic:

The weights multiply your predicted probability of an action, not counts of actions that already happened. Nothing in this formula reads "how many likes does this post have." It reads "how likely is this specific viewer to report this."

A report is far rarer than a like. That is why a much larger negative coefficient makes sense: a tiny predicted probability still needs enough scale to influence ranking. The coefficient does not grant one observed report the power of 468 observed likes.

This also changes how to think about brigading. The published weighted-score formula does not subtract a fixed amount from a post for every report. It uses a viewer-specific predicted report probability. Coordinated reports could still affect training, moderation, safety, or other systems outside this formula, so the repository does not support a broader claim that brigading has no effect.

The dwell detail

Notice that the flag for "did they dwell on this post" is weighted 0.0, while each second of dwell time is weighted 0.004.

The binary "did they dwell" term contributes nothing with its current weight, while predicted dwell duration contributes continuously. Pure coefficient arithmetic gives 125 seconds of dwell weight the same raw coefficient contribution as a hypothetical like probability of 1.0. Treat that as coefficient math, not a behavioral exchange rate. Model normalization and the predicted probability distributions determine the contribution seen for real requests.

The five things that happen after the sum

The weighted sum isn't the final score. Five adjustments run on top of it, in order, and this is where most of the feed's actual character comes from.

Post-sum ranking adjustments
The post-sum ranking chain adds exploration, author diversity, network weighting, and optional diversification around the model score.
  1. A floor for bad posts
    If the negative predictions outweigh the positive ones, the post's score gets compressed into a tiny band just above zero. It will always sort every non-negative post below, but it preserves the ordering among bad posts. A clean trick: the worst content is banished without the arithmetic exploding.
  2. Cold start exploration
    The scorer has an explicit exploration path for low-exposure content. Among eligible candidates from authors with fewer than 1,000 followers and posts with fewer than 1,000 impressions, one candidate can be lifted toward the score around feed slots 15 to 16. This gives low-impression posts a route into the ranked set instead of relying only on the normal ranker to surface them organically. There's also an optional mode, present in the code and currently off by default, where a multi-armed bandit samples from a probability distribution over candidate like rates and picks the most promising exploration candidate.
  3. Author diversity
    Your favorite poster published eight times this morning. Without intervention, several of those posts could crowd the ranked set.
    So each additional post by the same author gets multiplied down. The first keeps 100% of its score. The second gets 62.5%. The third 43.75%. The fourth 34%. It bottoms out at 25%, so a prolific author is penalized progressively rather than removed outright.
  4. The out-of-network discount
    Posts from accounts you don't follow get multiplied by 0.75.
    Here's the less obvious part: the same 0.75 also applies to replies and reposts from accounts you do follow. Original posts from followed accounts avoid this multiplier. That gives replies and reposts from followed accounts a concrete ranking disadvantage relative to their original posts.
    (Brand-new viewers also have a much more aggressive optional out-of-network multiplier of 0.00001, effectively producing an in-network-heavy feed until more personalization signal exists. That mode is off by default.)
  5. The variety re-shuffle
    Finally, DPP diversification is applied within a scope capped around rank 150. The selector balances two goals: keep high-scoring posts while penalizing candidates that are too similar to one another.
    A single knob, currently 0.65, controls the trade-off. Push it toward 1 and score dominates. Push it toward 0 and diversity matters more. Once the set is chosen, the winners are re-sorted by their original scores.

Case study: the mutual-follow boost

The repo ships with one change documented end to end, and it's the best window into how these dials actually get turned.

Here's what it does: for original posts (not replies or reposts) from people you mutually follow, a bonus is added on top of the base reply weight of 5. The bonus is currently 15, so those posts use a reply coefficient of 20 instead of 5.

Three conditions have to hold. Miss any one and the post is scored normally.
Three conditions have to hold. Miss any one and the post is scored normally.

Note:

X isn't boosting your friends' posts. It's boosting posts from friends that the model thinks you'll reply to.

The boost applies specifically to the reply-probability term. A mutual's post with a low predicted reply probability gets little from the bonus. A mutual's post with a high predicted reply probability benefits much more. The reply coefficient is 4× the normal value for this case, but the entire post score is not multiplied by four. It's a bet on conversation, not a blanket affinity boost.

Two rulebooks

Ranking decides order. A separate visibility system decides whether a post can be shown at all, and it answers one of three ways: allow it, put it behind an interstitial you can tap through, or drop it.

The design choice worth knowing: there are two rulebooks, and which one applies depends on whether you follow the author.

The same post, two different rulebooks. Following the author removes 26 checks.
The same post, two different rulebooks. Following the author removes 26 checks.

The phrase that unlocks this is "high recall."

A high-recall spam classifier is tuned to catch as much spam as possible, which also raises false positives. That trade-off is easier to accept when the system recommends a post from a stranger. For content from someone you deliberately followed, the same false positive would hide something you explicitly asked to receive.

The identical post can be dropped from a stranger's feed and shown to a follower.

That asymmetry matters when interpreting broad claims about "shadowbanning." The same visibility rule set does not apply uniformly to followed and recommended content.

Two more mechanics: the first rule says drop ends the evaluation, so later rules don't get a second vote. Drops also cascade: remove a parent post and dependent conversation content can disappear with it.

The three-minute window

You pull to refresh. You get a feed that looks suspiciously like the one you just had. This is why.

After a non-cached request, up to 750 positively scored candidates are compressed and stored in Redis for 180 seconds. On a later request, the cached path is accepted when at least 500 candidates are available. When that happens, the normal retrieval sources and Phoenix scoring path are bypassed for those cached candidates.

X For You feed cached refresh flow
Inside the cache window, a refresh can reuse the scored candidate pool instead of running the normal retrieval and model path again.

Inside three minutes, a refresh can reuse the same scored candidate pool. Once the cache expires, the normal retrieval and scoring path runs again.

And here's the part that makes this a nice piece of engineering rather than a shortcut.

Reusing a score is much safer when that score does not depend on which other candidates happened to share the batch. Phoenix enforces exactly that property: candidate posts cannot attend to one another during scoring. Each candidate attends to viewer context, not to neighboring candidates.

One benefit is stable per-candidate scoring. The same property also makes cached scores easier to reuse because one candidate's score does not change with batch composition. A model-level constraint therefore creates a useful serving-level optimization.

Ads get the last word

One more thing about ordering: the ranker's output isn't quite the order you see.

The default ad blender splits ranked posts into brand-safe and not-brand-safe groups, then builds ad and organic groupings from the safe supply. That means organic posts can be reordered after ranking is finished so ads only sit next to compatible content.

Who-to-Follow lands at slot 6. Prompts go at the top. A feed survey, when it appears, goes at slot 12.

Odd corners

  • A deterministic holdout. A hash of (post, viewer) puts each pair into a stable bucket from 0 to 99. A configured percentage can then be withheld consistently for experimentation. The default is currently zero, but the mechanism is present.
  • Only one branch of a conversation survives. If several posts from the same thread make it through, the highest-scoring one is kept, and the rest are discarded.
  • A reply whose parent failed to load is dropped. If the ancestor post didn't hydrate, the reply can't be shown in context, so it goes.
  • A filter for Brazil's 2026 elections, added for Brazilian electoral-law handling. It removes qualifying posts from recommendations under the configured rule, while treating followed-author content differently. The relevant statute is quoted in a comment above the implementation.
  • An entire alternative scoring mode exists, which scores each post relative to the batch average rather than on its own absolute merits. It's implemented, tested, and switched off.
  • Age limits are source-specific. The repository contains age constraints in several candidate paths, so a single universal "48-hour feed cutoff" is too broad a description of the current system.

Key takeaways

  • The like coefficient is small relative to several other positive actions. Reply and copy-link coefficients are 10× and 40× the like coefficient, but these numbers weight predicted probabilities rather than observed engagement counts.
  • Weights multiply predicted probabilities, not counts. "One report cancels 468 likes" is not how any of this works.
  • Original posts from people you follow are the only content that pays no tax. Replies and reposts from people you follow get the same 0.75 discount as total strangers.
  • Ranking and visibility are separate systems with separate rules, and the rules are stricter for posts from people you don't follow.
  • Refreshing inside the three-minute cache window can reuse the same scored candidate pool. Once the cache expires, the normal retrieval and scoring path runs again.

Tags:


Comments:

Please log in to be able add comments.