0x00 한

Research / Cross-service topics

Candidate allocation — what to include from multiple sources, and how many

Candidate allocation is the layer that decides, when there are 10 or 30 candidate generation sources each, "how many to take from each source and how to mix them into the final list." There are three approaches, and X changed its approach as generations changed. The 2025 Scala generation used caps, weaving, and priority merge verified(Allocation structure by generation). The 2026 Rust generation has no allocator and uses score multipliers verified(2026 Rust, 50:50 section).

  1. Ratio allocation (ratio): splits slots like "30% from this source." The composition of the result is guaranteed.
  2. Cap allocation (cap): takes at most N from each source and, after that, lets candidates compete by score only. The composition is not guaranteed.
  3. Interleave (round-robin / weave): picks one from each source in turn. It is used in combination with caps.

Through the Gen3 release, X and Meta published this layer in exactly opposite directions.

  • X published all of the allocator code. The cap values were outside the repo through Gen3 (0bfc279) and, since the Gen4 release (2026-08-13, 47c1bcd), can be read from the parameter mirror verified — home-mixer/params/param.rs.
  • Meta states the per-stage candidate counts as numbers for each surface, but has not published anywhere the allocator that produces those numbers.

This asymmetry is a recurring theme across these notes.

"X mixes in-network and out-of-network 50:50" is wrong

The claim that X mixes in-network (posts from followed accounts) and out-of-network posts half and half is the most widespread misconception. The two sentences used as evidence for it are both observed result ratios ("on average"), not quotas in code. verified — Twitter blog 2023-03-31 (checked against the archive 2026-08-09), twitter/the-algorithm README.md:41

  • The 2023 official blog wrote that this ratio is an average and can differ by user verified — Twitter blog.

    "the For You timeline consists of 50% In-Network Tweets and 50% Out-of-Network Tweets on average, though this may vary from user to user"

  • The README says that, in the search index row, about half of tweets come from this candidate source verified — same README.

    "~50% of Tweets come from this candidate source"

There is no code in the 2026 generation that enforces the ratio of in-network to OON (out-of-network, posts from accounts the user does not follow).

The multiplier value splits three ways verified — ranking_scorer.rs:220-239. In Gen3 (0bfc279) none of the three values was in the repo, and the Gen4 (47c1bcd) mirror has all three verified — params/param.rs, params/config.rs:38.

  • For a topic request it uses TopicOonWeightFactor. As of the Gen4 release the value is 0.5.
  • For a new user it uses NEW_USER_OON_WEIGHT_FACTOR. A new user is a user whose account age is less than NewUserAgeThresholdSecs and whose following count is at least NEW_USER_MIN_FOLLOWING. As of the Gen4 release the value is 0.00001.
  • Otherwise it uses OonWeightFactor. As of the Gen4 release the value is 0.75.

In other words, "how much OON to include" moved from an allocation problem to a ranking problem. The resulting ratio varies per request with the multipliers and the candidate pool.

↑ Contents

Allocation structure by generation — X

X's allocation structure differs by generation. The 2025 Scala generation used caps, interleave, and priority merge by pipeline order together verified(2025 Scala). The 2026 Rust generation has no allocator, has a cap per source, and its only global selection is by score order verified(2026 Rust). In both generations, expensive sources are not run when enough cached candidates exist verified(2026 Rust, home-mixer layer).

2026 Rust: no allocator, only per-source caps

As of the Gen3 release (2026-05-15) code, the 2026 generation has no allocator that divides shares among sources. Each source has a cap and the global selection is a single score-ordered step; effectively, the cache gate plays the allocator's role verified.

  • As of Gen3 (0bfc279) there are 11 sources verified — home-mixer/sources/
    • thunder_source / phoenix_source / phoenix_moe_source / phoenix_topics_source / tweet_mixer_source / cached_posts_source / scored_posts_source / ads_source / who_to_follow_source / prompts_source / push_to_home_source
  • Each source reads its own cap from a FeatureSwitch (runtime configuration). In Gen3 those values were outside the repo verified — thunder_source.rs:38, phoenix_source.rs:89, tweet_mixer_source.rs:61
    • The cap names are ThunderMaxResults, PhoenixMaxResults, and TweetMixerMaxResults.
    • The defaults in the Gen4 (47c1bcd) mirror are 1200, 1000, and 800, respectively verified — params/param.rs.
  • The global selection is a single step that picks the top K by descending score verified — top_k_score_selector.rs:13
    • The selector is TopKScoreSelector, and it picks TOP_K_CANDIDATES_TO_SELECT candidates.
    • This constant is also in the params module. The params module was not published in Gen3, and the value published in Gen4 (47c1bcd) is 50 verified — params/config.rs:17.
  • The cache gate effectively plays the allocator's role. When cached posts are at or above the threshold, all sources and Phoenix (X For You's ranking model) itself are quietly switched off verified.
    • That is, the primary decision on "how many new candidates to fetch" is the cache state, not the per-source cap.

2025 Scala: cap + interleave + priority weave

The 2025 generation is far more complex, and allocation happens in two layers. tweet-mixer, which produces OON candidates, uses ratio allocation only for low-signal users and uses blenders and caps for everyone else verified(tweet-mixer layer). home-mixer, which assembles the feed, uses pipeline order as priority verified(home-mixer layer).

tweet-mixer layer

tweet-mixer handles OON candidate generation exclusively. A Home request has 34 candidate pipelines wired in verified — HomeRecommendedTweetsRecommendationPipelineConfig.scala:271-332 (entries :274-331)

Allocation proceeds in five steps verified :362-412.

  1. For a low-signal user (a user with a small follow graph), it ends here. It dedups to 1 per author, performs explicit ratio allocation, and then blocks all later selectors verified.
    • The dedup is done by DropDuplicateCandidates(duplicationKey = _.features.getOrElse(AuthorIdFeature, None)) :364.
    • The ratio allocation is done by InsertAppendRatioResults.
    • The step that blocks later selectors is DropAllCandidates() :380.
    • The whole block is gated by AllowLowSignalUserGate.evaluate :382.
    • The default ratio values are 0.7 for Grok topics, 0.15 for SimClusters interested-in, 0.15 for popular regions, and 0.01 for all the rest verified TweetMixerGlobalParams.scala:225,233,241,249.
    • It is applied in bulk to "the rest" via filterNot :372-378. These values are defaults, not production values.
  2. If the user is not low-signal, one blender is chosen and run. 3 blenders are wired into the Home pipeline, and BlendingParam picks one of them verified :399,401,402.
    • exclusiveRoundRobinSelector weaves by (source, signal) buckets.
    • signalPrioritySelector adds USSFeatures.getPriority to the bucket criteria.
    • weightedSignalPrioritySelector applies weights when weaving. The seed is fixed with new Random(0) verified TweetMixerFunctionalComponents.scala:289.
    • However, these are not the only blenders. TweetMixerFunctionalComponents.scala defines many more selectors, and BlendingEnum has 6 values verified TweetMixerGlobalParams.scala:23-30.
      • The defined selectors include roundRobinSelector :210 and inclusiveRoundRobinSelector :237.
      • The correct statement is not "there are 3 blender types" but "this surface wired up 3."
  3. The result is truncated at the light-ranker cap. The default value is 500 verified HomeRecommendedTweetsParam.scala:9-15.
    • The truncating step is DropMaxResults(LightRankerMaxResultsParam) :404.
  4. Then 7 u2i (user→post) pipelines are appended by round-robin verified.
    • The pipelines are at :385-393, and the selector that appends them is inclusiveRoundRobinSelector :405-406.
  5. Finally, the result is truncated to the smaller of the requested count and the server cap verified :408-411.
    • This step is DropRequestedMaxResults, and it applies min(requestedMaxResults, serverMaxResults).
    • The parameter names for the two values are DefaultRequestedMaxResultsParam (300) and ServerMaxResultsParam (500) verified HomeRecommendedTweetsParam.scala:18-24,27-33.

Step 4 looks like preferential treatment, but it is not. It is easy to read it as "it is appended after the cap, so it is exempt from slot competition," but two facts overturn that conclusion verified.

  • The truncation in step 3 cuts only the result list and does not discard the remaining candidates verified DropMaxResults.scala:36-38. So the u2i candidates appended afterward are placed at the very end of the list.
    • In code, DropMaxResults truncates only result and leaves remainingCandidates as is.
  • The number home-mixer actually requests is 400 verified. It is smaller than the light-ranker cap of 500.
    • The evidence path is ScoredTweetsParam.scala:176 (default 400) → ScoredTweetsTweetMixerCandidatePipelineConfig.scala:77 → HomeRecommendedTweetsProductPipelineConfig.scala:73.

That is, because the final cut line of step 5 (400) is tighter than the cap of step 3 (500), if the non-u2i blend alone fills 400, none of the appended u2i candidates remains. u2i does not compete for the 500 slots; instead it is the first to be cut at 400. Being appended later in the order is lower priority, not preferential treatment.

The low-signal user determination has a place where the code and the comment disagree. The gate comment says "< 25 users", but the code is SmallFollowGraphSize = 5. This mismatch is duplicated identically in both tweet-mixer and home-mixer. verified

  • tweet-mixer: SignalUtils.scala:17,20 vs AllowLowSignalUserGate.scala:12·DenyLowSignalUserGate.scala:12
  • home-mixer: util/SignalUtil.scala:18 vs scored_tweets/gate/DenyLowSignalUserGate.scala:13

Quoting the comment gets the threshold wrong by a factor of 5. This threshold is the switch that decides "whether to fall back to explicit ratio allocation," so the number of affected users differs greatly depending on whether it is 5 or 25.

home-mixer layer

home-mixer has 9 candidate pipelines, and their order itself is the allocation rule verified. The list and order are in ScoredTweetsRecommendationPipelineConfig.scala:446,451-459.

Static → CachedScoredTweets → EarlybirdInNetwork → DirectUteg → TweetMixer
       → ContentExploration → Lists → Backfill → Communities

The code put this comment directly above the list: "Order matters. Duplicated candidates take the first occurrence in the pipelines when merger". Merging is handled by DropDuplicateCandidates(AllPipelines, IdAndClassDuplicationKey, PickFirstCandidateMerger) verified :462-469.

That is, this rule is neither a ratio nor a cap; it is a priority in which the earlier pipeline wins on duplicates. If the same post comes from both in-network and tweet-mixer, it is recorded as the in-network one.

Here too, the cache state is the gate.

  • The Frs, TweetMixer, and Uteg pipelines run only when the cached count is below MinCachedTweetsParam (default 30 :272) verified — ScoredTweetsParam.scala:269-275
    • Additional evidence is ScoredTweetsTweetMixerCandidatePipelineConfig.scala:51.

Fail-open (a policy that lets the response go out even if one pipeline fails) is the default, but there is one exception.

  • tweet-mixer applies fail-open mechanically to every pipeline verified HomeRecommendedTweetsRecommendationPipelineConfig.scala:334-337.
    • The applying code is candidatePipelines.map { … FailOpenPolicy.Always }.
  • home-mixer lists the fail-open targets by hand and registered only 8 of the 9. scoredTweetsContentExplorationCandidatePipelineConfig is not in the list verified ScoredTweetsRecommendationPipelineConfig.scala:456 vs :492-502.
  • A pipeline missing from the list follows the default policy verified product-mixer/core/.../RecommendationPipelineConfig.scala:113. So if ContentExploration dies, the whole request fails.
    • The default policy is defaultFailOpenPolicy = FailOpenPolicy(Set(ClosedGate)).
    • This contrasts with the other 8, where if one dies its share quietly disappears and is filled by the rest.

The code gives no clue whether this is intentional or an omission, but it is a form in which one pipeline is easily left out when writing 8 by hand, and it is hard to find a reason to leave only the exploration pipeline fail-closed inferred.

Allocating by "slot" rather than by source

There are also mechanisms that fix positions instead of leaving all candidates to score competition. These are position reservations, not ranking boosts. They do not raise a score; they assign the slot in advance.

  • Candidates per community are 1, and the total of community candidates is 3 verified KeepTopKCandidatesPerCommunity.scala:14-15,30,32
    • The constants are MaxCandidatesPerCommunity = 1 and MaxCommunityCandidates = 3.
  • Content exploration candidates and DeepRetrieval (a candidate source that trains a user tower and an item tower separately and scores by dot product) candidates are pulled up to specific positions verified ScoreAveragingPositionSelector.scala:121,137,193
    • The parameters are ContentExplorationBoostPosParam and DeepRetrievalBoostPosParam, and each has getNumCandidatesToBoost = 1.

↑ Contents

Case: Meta — numbers exist but no allocator

Meta published in the opposite direction from X. It states per-stage candidate counts as numbers for each surface but does not publish the allocator that produces them, and the only surface where an allocation mechanism looks mechanical is Instagram inferred(Meta-side allocation mechanisms).

pass 0 candidate counts by surface

Meta lists per-surface candidate counts in the AI system card (the ranking explanation document Meta publishes for each surface). The pass 0 (the stage that reduces candidates with a lightweight model) count differs by surface, so writing "Meta's pass 0 is 500" in one line is right for only one of nine rows.

Surface pass 0 / retrieval Next stage Reduction ratio
Facebook Feed about 500 about 500 1:1
Facebook Video about 1,000 top 200 5:1
Facebook Reels about 10–100 (no figure) —
Facebook Feed Recommendations no figure — —
Instagram Feed about 700 about 500 1.4:1
Instagram Explore up to 1,500 top 100 15:1
Instagram Reels Chaining about 100 about 100 1:1
Instagram Feed Recommendations no figure — —
Threads For You no figure — —

Each figure is verbatim from the AI system card body verified index (2023 onward). Original quotes and surface context are in facebook.md, instagram.md, and threads.md.

  • Surfaces with the same name have different numbers. FB Feed does not shrink going from 500 to 500, whereas IG Feed shrinks from 700 to 500. Meta's documents describe a different funnel shape for each surface.

  • Unconnected (Recommendations) cards have no figures on either FB or IG. However, the 2023 blog describing the same system says it narrows billions to thousands and then to a few hundred verified ai.meta.com, 2023. Two of Meta's own documents describe it at different resolutions.

    "narrow billions of pieces of content down to thousands and then to a few hundred"

  • The Threads card gives no per-stage figure at all. observed 2026-08-09 (1 instance in the ig-threads-feed card body)

The two sets of numbers differ in nature — this is the real structural difference

The candidate counts of X and Meta differ starting from where they come from. X's numbers come from source code, and Meta's from system cards observed. In practice, Meta's numbers can be used for relative comparison (Explore 15:1 vs Feed 1.4:1) but not for reproduction. X's numbers are the opposite.

  • X's numbers come from source code. They are exact, one can cite the file and line number, and even the inconsistencies show (the 400 cut line behind the 500 cap seen above). verified
    • Examples: LightRankerMaxResultsParam 500, DefaultRequestedMaxResultsParam 300, ServerMaxResultsParam 500, the home-mixer actual request value 400, MinCachedTweetsParam 30
  • Meta's numbers come from system cards. All are rounded values with "approximately", "about", or "up to" attached, and they disclose neither the truncation order nor who cuts where. observed 2026-08-09 (corpus inspected: the 9 system card bodies in the table above)
  • The purposes of the two publications differ inferred. The X repo through Gen3 was meant to make the system readable, so the logic is 100% there and the values 0%, while the system card is a document answering the DSA (EU Digital Services Act) explanation obligation, so it gives rounded values and the logic is 0% inferred. The basis is that the precision patterns of the two publications point in exactly opposite directions.
    • Since the Gen4 release (47c1bcd), the X repo has the values too verified(2026 Rust section).

Meta-side allocation mechanisms — mechanical on only one surface

The only Meta sources where an allocation mechanism appears mechanically are Instagram's 2020 retrieval DSL and the 2023 Explore post. These two sources show per-source weights, per-account caps, round-robin by seed account, and merge priority verified. The full query is in instagram.md.

  • The Explore post says it mixes 4 source types (heuristics, ML, real-time, pre-generated) with tunable weights verified Scaling the Instagram Explore recommendations system, 2023.

    • The original wording is: "mix them with tunable weights".
    • This is a statement that explicitly says per-source weights exist as tuning parameters. The values are not published.
  • The retrieval DSL has a per-account cap. It is the same type as X's per-source cap (cap allocation), but the key is the author, not the source verified 2020.

    • The Instagram-side parameter is max_media_per_account=10, and the X side is the *MaxResults family.
  • The retrieval stage also has round-robin. It is the same type as X's source×signal bucket weave, with only the key differing: the seed account verified 2020.

    • On the Instagram side it is diversify_by(seed_id, method=round_robin), and on the X side exclusiveRoundRobinSelector.
  • Seed sources have a merge priority verified 2020.

    "The final merge order is as follows: H >> R > F."

    • H comes from Home Feed, R from other recommendation surfaces such as Explore/Reels, and F is the graph exploration backup.

Merge priority works in almost the same way as X. X home-mixer chains 9 pipelines in order and added the comment "Order matters. Duplicated candidates take the first occurrence in the pipelines when merger". Instagram published the same rule as a single sentence, H >> R > F. A priority allocation of "the earlier source wins on duplicates," neither ratio nor cap, exists on both sides. inferred We cannot confirm whether the implementations are the same. Instagram's code is not public.

Handling of low-signal and new users also corresponds. Both fall back to an explicit distribution constraint only when score competition is meaningless.

  • X falls back to explicit ratio allocation only for low-signal users verified(2025 tweet-mixer).

  • Instagram builds seeds for users with sparse history by one-hop/two-hop graph expansion, and applies as a training constraint a condition that keeps the distribution from skewing away from Home-based sources verified 2020.

    "the overall distribution is not skewed away from Home-based sources"

Facebook has no such mechanism.

  • We found no primary source for a per-source candidate count breakdown of Facebook Feed retrieval (no primary source found). inferred
    • The corpus inspected was 15 Transparency Center system cards, the bodies of engineering.fb.com, ai.meta.com, and about.fb.com, and earnings call transcripts (read 2026-08-09).
  • The likely reason is that the connected-content corpus is already limited to the friend graph, so the allocation problem itself is not large inferred.

↑ Contents

Case: Netflix — the allocation unit is the row, not the source

Adding Netflix makes one premise of these notes conditional. The question "how many to mix from each source" only makes sense when the corpus is large. Netflix has no item-level allocation, and its allocation unit is the row verified.

There is no item-level allocation layer. Because the catalog has thousands to tens of thousands of titles, the PVR (the ranker that fills genre rows) "sorts the whole catalog (or a genre-filtered subset) per member" verified TMIS 2015.

  • If every item can be scored, neither a per-source cap (X's *MaxResults) nor a funnel figure (Meta's pass 0) is needed.
  • There is no place for numbers like X's 500/400 to appear.

Instead, the same problem reappears one layer up, in row allocation. Each member has tens of thousands of candidate rows ("tens of thousands"), and the surface has about 40 rows with up to 75 titles per row verified TMIS 2015. The question "how many from which source" becomes "how many rows of which kind (genre/BYW/Trending/CW), in what order". BYW is the Because You Watched row, and CW is the Continue Watching row.

In 2015, Netflix had already abandoned ratio allocation (templates) and switched to ML-based selection verified. ML-based selection allows "homes with zero BYW rows, and homes where half the rows are BYW" verified. In effect, Netflix made at the row level, 10 years earlier, the switch X made in 2026 (removing quotas → score competition) inferred. This means the problem structure is the same, not that there is a lineage relationship.

Per-row-type rankers correspond to X's candidate sources. PVR, Top-N, Trending, Continue Watching, and sims each fill row contents with a different algorithm verified TMIS 2015, and a page algorithm assembles them. This is the same role as X home-mixer merging 9 candidate pipelines. The difference is in the merge rule. X uses the priority rule "on duplicates, the earlier pipeline wins", and Netflix uses an optimization, row selection.

GenPage (2026) unifies this entire layer into a single model. It replaces row selection and within-row ordering with autoregressive generation by one Transformer, and end-to-end latency is −20% verified arXiv 2606.31031 (experimental stage). This is the same direction as Meta's SilverTorch unifying the index into model tensors. Allocation logic moves from explicit rules to a learned distribution.

Because no ads ranking model material has been released, the allocation rules between organic and ads are also unknown verified (absence). Related statements are in netflix.md.

↑ Contents

Ads vs. organic — count slots or pixel distance

The allocation between ads and organic is where the two companies differ most clearly in the allocation layer. X inserts ads by count after organic ranking finishes, and a Meta patent converts ads and organic to a common unit and puts them in one ranking verified.

X inserts ads by count after organic ranking finishes. verified 2026 source code xai-org/x-algorithm

  • To attach ads, there must be at least 5 organic posts verified ads/util.rs:10
  • The default spacing is {requested: 3, min: 2} verified ads/util.rs:12-17
  • It inserts only up to half of the safe gap verified ads/partition_organic_blender.rs:35
    • The formula is max_from_safe = safe_count / 2.
  • The blender selector deliberately disables the framework's sorting, then inserts prompts, follow recommendations, and ads at designated positions verified blender_selector.rs:25-114
    • The selector is BlenderSelector, and the way it is disabled is setting score() to the constant 0.0.

That is, ads do not take part in the ranking competition. They are inserted into the already sorted list by a count rule.

Meta converts ads and organic to a common unit and puts them in one ranking. verified US 10,345,993 B2 "Selecting content items for presentation in a feed based on heights associated with the content items", filed 2015 / granted 2019, patent

  • The patent states that it converts organic scores and ad scores to a common unit of measurement, then ranks ads and organic stories together in one ranking verified patent.

    "Scores associated with organic news feed stories and scores associated with advertisements are converted into a common unit of measurement, and advertisements and organic news feed stories are together ranked in a single ranking."

  • The spacing constraint is expressed in both count terms and pixel-height terms verified patent.

    • Count-based: "at least a threshold number of organic news feed stories are presented between advertisements"
    • Or height-based: "a combination of heights of the organic feed stories exceeds a gap distance threshold"
  • The position discount (the decrease in predicted interaction by position) depends on the sum of the predicted heights of the candidates placed above verified patent.

  • Height prediction itself is an ML model. It predicts the height at which content will be displayed from content type, language, and comment count, plus client screen size, OS, resolution, and app version verified patent.

    "the likely height of the content item when presented"

That is, interleaving is a geometric constraint, not a slot index. Because the rule is "at least pixel distance X from the previous ad" rather than "one in every 3", even for the same user the actual number of ads varies with screen size and content-type mix inferred. Modeling it with a fixed slot ratio (ad load N%) would be wrong inferred. However, the patent does not state the resulting share.

⚠ Two caveats apply.

  1. This patent was filed in 2015 and is not evidence of current production. However, it is the only mechanical primary source that remains on Meta ads interleaving.
  2. The widely circulated Total Value = Bid × Estimated Action Rate × Ad Quality must not be cited as a formula, because no Meta primary source was found. The same holds for ad load figures and interleaving rules inferred.
    • The corpus checked was the Marketing API bidding and delivery docs, the Transparency Center ads pages, and about.fb.com posts (read 2026-08-09). Details are in facebook.md.

Threads rolled out ads globally in 2025 Q3, but did not disclose the interleaving rules, and it is also unknown whether it applies the pixel-height patent. inferred (same corpus, 2026-08-09). Related statements are in threads.md.

↑ Contents

What generalizes from this case

When reading allocation, look at the actual path in the code rather than the published ratio. The actual source mix depends on cache state, fail-open policy, and the order of cutoffs. Whether allocation is needed, and its unit, follow corpus size and surface structure.

  • Where is allocation actually decided?
    • A published "ratio" is usually an observed result, not a quota in the code. You have to open the source and find the allocator yourself. X eventually removed the allocator and moved to score multipliers.
    • The cache is the biggest allocator. In both X generations, the first branch is "if there are enough cached candidates, skip the expensive sources". That is, the actual source mix varies with how often the user makes requests.
    • Ratio allocation is used only for new and low-signal users. It falls back to explicit quotas only when there are no signals and score competition is meaningless. X uses explicit ratios for low-signal users, and Instagram uses seed expansion and a Home distribution constraint for sparse users. The same pattern is observed on both sides.
  • How do caps and failure policy change allocation?
    • When fail-open is the default, allocation is a tendency, not a guarantee. In X, nearly every candidate pipeline is fail-open, so a response goes out even if one source dies, and its share is quietly replaced by other sources. However, if the policy is listed by hand, the one omitted becomes fail-closed. X home-mixer's ContentExploration is that case. The difference between mechanical blanket application (tweet-mixer) and manual listing (home-mixer) becomes directly a difference in failure blast radius.
    • When caps are layered, "attached later" means lower priority, not preferential treatment. In X 2025 tweet-mixer, the u2i candidates are attached after the light-ranker cap (500) cutoff and appear to be exempt from competition. But the final cutoff after that is 400, so if the earlier part is full, they disappear first. If you conclude from looking at only one cutoff, the direction comes out reversed.
  • How should numbers and models about allocation be read?
    • For a "candidate count" number, first look at the kind of source. Numbers from source code are exact, usable for reproduction, and even reveal mismatches between them. Numbers from regulatory documents are rounded and usable only for relative comparison. Precision is determined by the purpose of the release, not by the nature of the system.
    • Modeling ad interleaving as a slot ratio may be wrong. X is count-based (spacing 3, at least 5 organic), so a slot model fits. The Meta patent uses a common unit of pixel height and a gap distance constraint, so results vary with screen size. The same summary "one ad every N items" is right for one and wrong for the other.
  • What determines whether allocation is needed, and its unit?
    • The existence of an allocation layer is itself a function of corpus size. If the catalog is small enough to score every item (Netflix, thousands to tens of thousands), item-level allocation disappears and the problem moves one layer up (row composition). Before asking "what is this service's candidate allocation?", ask "is the scale one that needs allocation?"
    • The allocation unit follows surface structure. A single stream allocates items, and a 2-dimensional page allocates rows. Netflix's row types (genre/BYW/Trending) play the same role as X's candidate sources. However, only the merge rule differs: priority (X) versus optimization (Netflix).

↑ Contents

Who uses this

For X, all allocation logic is in the repo, and the values were outside the repo through Gen3 and have been in the repo's parameter mirror since the Gen4 release (2026-08-13) verified. Facebook and Instagram disclose candidate counts for several surfaces, and Threads discloses none observed. Netflix allocates rows, not items verified.

  • X — The 2025 Scala generation uses caps, weaving, and priority merging, and the 2026 Rust generation uses score multipliers with no allocator. All logic is in the repo; the values were outside the repo in 2025 Scala and Gen3, and have been in the repo's parameter mirror since the Gen4 release (47c1bcd) verified — x.md
  • Facebook — pass 0 is about 500 (Feed), 1,000→200 (Video), and 10–100 (Reels), and unconnected cards have no figures. There is no primary source for the per-source breakdown. The evidence on the ads side is a patent that uses pixel height as a common unit verified — facebook.md
  • Instagram — Feed is 700→500, Explore is 1,500→100 (15:1), and Reels Chaining is about 100. It is the only Meta surface where the allocation mechanism looks mechanical (tunable weights, max_media_per_account, seed round-robin, H >> R > F). verified — instagram.md
  • Threads — There are no per-stage candidate counts at all. In the inventory definition, the card states only the asymmetry that follows are all and public content is a portion observed 2026-08-09 (1 ig-threads-feed card body), threads.md
  • Netflix — There is no item-level allocation (the whole catalog is scored), and the allocation unit is the row. It narrows tens of thousands of candidate rows to about 40 rows. It dropped templates (ratio allocation) in 2015 verified — netflix.md
  • Other services — Not yet investigated. They remain open questions.

↑ Contents

Not yet verified

On the Meta side, we have not yet confirmed the merge rule for connected and unconnected results, how Explore weights are tuned, whether the height patent is currently in use, or Threads' per-stage figures. On the Netflix side, we could not confirm the current row-selection algorithm.

  • By what rule does Meta interleave connected (500) and unconnected (several hundred) results? It says only that the two systems are separate and does not explain the merge rule. See: facebook.md open question 4.
  • What is used to tune Instagram Explore's per-source tunable weights? It says only "tunable" and does not disclose the objective function or the optimization method.
  • Is Meta's pixel-height patent (filed 2015) still in operation in 2026? There is no primary evidence beyond the patent.
  • Threads' candidate generation approach and per-stage figures. The system card gives no numbers at all.
  • What form does Netflix's row-selection algorithm take now? The 2015 paper describes row-level scoring and a greedy-type approach. There is no primary source for the 10 years between then and GenPage (2026 experiment).

↑ Contents

Sources

Public source code

  • xai-org/x-algorithm — 2026, public source code (Apache 2.0). home-mixer/sources/, home-mixer/selectors/, home-mixer/ads/. The reference commit for the line-number quotes in the body is 0bfc279 (Gen3), and the Gen4 values were read at 47c1bcd.
  • twitter/the-algorithm — commit c54bec0 of 2025-09-03, public source code. tweet-mixer/, home-mixer/.

Meta Transparency Center (primary source for candidate counts)

Meta blog posts and patent

Netflix

↑ Contents