Supplement · The X algorithm · page 9 of 11
Blending: a feed is not made of posts alone
Ranking is over, the posts are ordered. But the screen will also hold ads, the «who to follow» block and promo prompts — things that are not ranked by the model and should not be. Let us see how that is mixed in: why ads are placed by rules rather than by score, where the limit on their number comes from, and what happens after the answer has been sent.
In brief
- Ads are placed by rules, not by score. Their place is determined by spacing and by constraints rather than by comparison with organic posts.
- The number of ads is limited by three things at once: how many there are at all, the spacing step, and how many brand-safe posts there are in the feed.
- No more than half of the safe posts may sit next to an ad — a hard ceiling in the code.
- If there are fewer than five posts, there will be no ads at all. An empty feed is not filled with advertising.
- Modules stand at fixed positions: «who to follow» is sixth, with a fatigue of 30 hours.
1. Why ads do not take part in ranking
The temptation is obvious: a post has a score, an ad has a bid — bring them to a common scale and sort them together. That is sometimes done, and it is called a unified auction. Here a different path was taken, and there are reasons for it.
- The quantities have no common unit. The score of a post is a weighted sum of action probabilities, a number of the order of hundredths. An advertiser's bid is money. The coefficient converting one into the other does not follow from anything: it has to be assigned, and it then becomes the main knob of the whole economics of the product.
- Ads carry obligations. A certain number of impressions has been promised to the advertiser, and adjacency to certain content is forbidden by contract. Such constraints are expressed as rules rather than as terms in a score: a score can be outbid, an obligation cannot.
- The share of ads is a business decision, not a prediction. How much of it there is in the feed is determined by the company's strategy. Handing that to a model means losing control: it will choose the share that is optimal for its own metric, not for the goals of the product.
We formulated the same conclusion in the chapter «How to design a system from scratch»: quantities with different objective functions are not combined into one score, they are spread across stages and tied together by rules.
2. How many ads will fit
The blender's main function reads almost like a formula:
let spacing = compute_spacing(&ads);
let safe_count = scored_posts.iter().filter(|p| !has_avoid(p)).count();
let max_from_safe = safe_count / 2;
let expected_from_spacing = n.saturating_sub(1).checked_div(spacing.requested).unwrap_or(0);
let actual_ads = ads.len().min(expected_from_spacing).min(max_from_safe);
A fragment of partition_organic_blender.rs · code by X, Apache 2.0, commit 28e414f
The number of ads is the minimum of three constraints:
| Constraint | Where it comes from | What it means |
|---|---|---|
ads.len() | The auction | You cannot place more than there are |
expected_from_spacing | \((n-1) / \text{step}\) | Ads must not come more often than the given step |
max_from_safe | \(\text{safe posts} / 2\) | No more than half of the safe surroundings is taken by ads |
Plus a check before all of that: if there are fewer than five posts, or no ads at all, the blender honestly returns a feed without ads and records the reason in a metric.
The function has_avoid marks posts next to which advertisers ask not to be placed: contentious topics, heavy content, everything a brand wants to distance itself from. Such posts count as «unsafe surroundings».
The constraint safe_count / 2 means: however many ads have been bought and whatever step is requested, there cannot be more of them than half the number of safe posts. In numbers: if the feed has 35 posts and 20 of them are safe, the ceiling is 10 ads, even if the auction supplied 30.
The economic meaning: a feed with bad content automatically loses the ability to be monetised. That is not a moral stance but a mechanism built into the code — the quality of the organic output directly limits the advertising inventory.
Note how much stronger this is than «try not to put ads next to bad things». Here bad content reduces the total number of ads, not merely their placement.
3. Three blenders to choose from
There are several implementations, switched by a parameter. By default partition_organic_low_risk works.
| Blender | The principle |
|---|---|
PartitionOrganicAdsBlender | Splits the organic feed into sections and inserts ads between them. Works by default |
SafeGapAdsBlender | Watches over a safe gap between ads |
TimeGapAdsBlender | Counts the gap not in posts but in viewing time: the parameters set a target interval in seconds and a minimum organic gap |
The third deserves a comment. Its parameters — a target interval of 4 seconds, a minimum organic gap of 3 posts, multiplier bounds from 0.5 to 2.0 — show that the distance between ads is measured not in positions but in the presumed time a person will spend on the interval.
The logic is clear: three short text posts are scrolled past in a second, while three videos take a minute to watch. The same gap in positions gives a completely different perceived frequency of advertising. By measuring in time, the blender evens out precisely the perception.
4. Modules at fixed positions
The «who to follow» block is inserted not by score but at a predetermined place:
fn insert_who_to_follow(blended: &mut Vec<FeedItem>, wtf_modules: Vec<WhoToFollowModule>) {
let Some(wtf) = wtf_modules.into_iter().next() else { return; };
let insert_idx = WHO_TO_FOLLOW_POSITION.saturating_sub(1).min(blended.len());
blended.insert(insert_idx, FeedItem { position: WHO_TO_FOLLOW_POSITION as i32, ... });
}
A fragment of blender_selector.rs · code by X, Apache 2.0, commit 28e414f
The constant WHO_TO_FOLLOW_POSITION equals 6. That is, the block with account recommendations is always sixth, if it is shown at all. Beside it lies a fatigue parameter: WhoToFollowFatigueHours = 30 — no more often than once in thirty hours.
Because the task is a different one. A score answers the question «how much better is this element than that one». For the «who to follow» block such a question is meaningless: it does not compete with posts, it does something else — it widens the follow graph on which the whole future feed depends.
Position six is a compromise readable without any code: not the first place, where the block would irritate, but within the first screen, where it will be seen.
The fatigue of 30 hours solves the other half of the task. The value of the block is not in showing it as often as possible but in it working at least once. Showing it on every visit is a guaranteed way to breed blindness to it. Note the oddness of the number: 30 hours, not 24. An interval that is a multiple of a day would tie the impression to one and the same time of day; 30 hours give a drift over time, and the block lands in different contexts.
5. Side effects: thirteen tasks after the answer
The answer has gone to the user. Then thirteen tasks start in the background, and their set shows well what the system actually needs from every request.
| Task | Who needs it |
|---|---|
| Publishing the identifiers that were shown | The next request — so as not to show the same things |
| The cache of ranked posts | The next screen of the same scroll |
| The cache of the request to the model | Saving calls on a repeat |
| Events for the re-ranking logs | The training of future models |
| Logs of the ad placement | Billing and reporting |
| Statistics by author | Analytics of reach |
| Statistics on mutual follows | Evaluating the effect of the corresponding boost |
| Metrics of scores and response sizes | Monitoring |
| Experiment logs | The analysis of A/B tests |
The first row is the most important and the most fragile. As we discussed on the page about the pipeline, these tasks are started via tokio::spawn and their result is not checked. Hence the four independent mechanisms guarding against a repeat impression.
Note the row about the re-ranking logs. Those are training data for future models, and they are written in the background by the same unreliable means.
Losing part of those logs will break nothing today — but it biases the sample the next model is trained on. If the losses correlate with load, and load correlates with the time of day, then the peak hours are systematically under-represented in the training data. This is exactly that gap between what was shown and what was logged, and it cannot be diagnosed by online metrics at all.
Common mistakes and hidden rocks
- Combining ads and organic into one auction «because it is more efficient». The quantities have no common unit, ads carry contractual obligations, and the share of ads is a business decision rather than a model's prediction.
- Placing modules by score. The «who to follow» block does not compete with posts: it widens the follow graph on which the whole future feed depends.
- Measuring the gap between ads in positions. Three text posts and three videos are completely different amounts of time. That is exactly why one of the blenders counts the gap in seconds.
- Forgetting about a module's fatigue. Showing it on every visit breeds blindness. And the interval is better made not a multiple of a day, otherwise it ties itself to one time of day.
- Considering logging reliable. The training data is written in the background with no check of the result; losses bias the sample for future models and are invisible in online metrics.
Interview questions
How do you build ads into a ranked feed?
As a separate stage after ranking, by rules rather than by a common score. The reasons: the score of a post and an advertiser's bid have no common unit of measurement, and the conversion coefficient would have to be assigned arbitrarily; ads carry contractual obligations that cannot be outbid by a score; the share of ads is a strategic decision that cannot be handed to a model.
In the system we looked at, the number of ads is the minimum of three constraints: how many there are, what the spacing step allows, and how many brand-safe posts there are in the feed, the last of which gives a ceiling of one half. Plus a threshold: if there are fewer than five posts, there are no ads at all.
Why limit ads by a share of the «safe» posts?
To honour the obligations to advertisers about adjacency and at the same time to tie monetisation to the quality of the output. The mechanism is hard: bad content reduces not the placement but the total number of ads placed.
Economically that means a feed with problematic content automatically loses the ability to earn. Such a feedback loop works more reliably than any declaration, because it is built into the code rather than into a policy.
How often should a block of account recommendations be shown?
Not by score but at a fixed position with a fatigue mechanism. In the system we looked at the position is the sixth — within the first screen but not in first place — and the interval before showing it again is 30 hours.
Its not being a multiple of a day is no accident here: an interval of 24 hours would tie the impression to one and the same time of day, while 30 hours give a drift, and the block lands in different contexts. The sense of the restriction is that the module's value lies not in the frequency of impressions but in it working at least once; a daily impression would breed blindness.
What should happen after the answer has been sent to the user?
Everything the current request does not need but the next ones do: recording the impressions, updating the caches, publishing the training logs, metrics, events for billing. In the system we looked at there are thirteen such tasks, and they start in the background without waiting for a result.
A mandatory consequence to keep in mind: since the result is not checked, all of it is unreliable. That is why several independent mechanisms are kept as a guard against a repeat impression. And for the training logs there is no reliable solution at all — losing them biases the sample for future models and is invisible in any online metric.
One-screen cheat sheet
Ads
Placed by rules after ranking, not by a common score with the posts.
How many
The minimum of three: how many there are · the spacing step · half the safe posts.
The threshold
Fewer than five posts — no ads at all.
Brand safety
Bad content reduces the total number of ads, not only their placement.
Three blenders
By sections (the default), by a safe gap, by viewing time.
The gap in time
A target interval of 4 s, a minimum of 3 organic posts between ads.
Who to follow
A fixed position 6, a fatigue of 30 hours — deliberately not a multiple of a day.
Side effects
13 background tasks after the answer. The result is not checked.
The hidden risk
The training logs are written just as unreliably; losing them biases future models.
Primary sources
- blender_selector.rs — the mixing, the fixed positions of the modules.
- partition_organic_blender.rs — the three constraints on the number of ads.
- time_gap_blender.rs — the gap measured in viewing time.
- side_effects/ — all thirteen background tasks.
- The course: the chapter «How to design a system from scratch» on layers with different objective functions, the chapter «Logging and its gaps» on logging and its gaps.