RecSys · textbook
Trainer Widgets Revision About All chapters ← Blending Correspondences →

Supplement · The X algorithm · page 10 of 11

Configuration: how the system changes without changing

One hundred and ninety-one parameters are read from an external configuration system rather than being sewn into the code. That makes it possible to change the behaviour of the feed without shipping — and that is also what makes the question «what is in production right now» non-trivial. Let us go through the mechanism, its price, and the real history of one parameter laid out day by day.

In brief

  • 191 parameters are declared by the param! macro: a name, a type, a key in the external configuration, a default value.
  • The defaults in the repository are synchronised with the production ones by a separate script — that is the way to make open code truthful rather than decorative.
  • Experiments are set through a stage's switch: every stage of the pipeline has an «is it on» method, inside which a parameter is read.
  • The history of one change is published day by day — from the launch of an A/B test to the final value, including what was tested and did not roll out.
  • Open code does not equal «here is what you have now»: part of the traffic is always in experiments, and a default reflects the main value, not the only one.

1. How a parameter is declared

param!(FavoriteWeight, f64, "rust_home_mixer_favorite_weight", 0.5);
param!(AuthorDiversityDecay, f64, "rust_home_mixer_author_diversity_decay", 0.5);
param!(EnableInventoryHoldout, bool, "rust_home_mixer_enable_inventory_holdout", false);

A fragment of param.rs · code by X, Apache 2.0, commit 28e414f

Four parts: the name of the type in the code, the type of the value, the key in the external configuration system and the default value. A parameter is read like this: query.params.get(FavoriteWeight).

The key thing is that the value arrives with the request rather than being taken globally. That is not an implementation detail but what the experiments rest on: for two simultaneous requests from different users the value of one and the same parameter may differ.

Why parameters rather than constants

The difference between const FAVORITE_WEIGHT: f64 = 0.5 and a parameter is the difference between «to change it is to ship a new version» and «to change it is to change a number in the configuration».

For a recommender system that is fundamental. Testing the hypothesis «replies are worth valuing above likes» with a constant means: write code, pass review, build, roll out to a canary, roll out fully, wait, gather metrics, roll back if it is bad. Days. With a parameter — minutes, and on any share of the traffic.

It also solves the problem of rolling back. A bad change in code is rolled back by a new release; a bad parameter value by restoring the old number, which happens instantly and requires no build.

The flip side is that 191 parameters are 191 ways of changing the behaviour of the system while bypassing code review. The discipline shifts from the code into the process of working with the configuration.

2. How the defaults stay true

If the values live in an external system, then the numbers in the repository are just placeholders, and the open code shows something other than what is running. That hole was closed explicitly: the README says that a script runs on a schedule bringing the defaults in the repository to the main production values.

That is worth appreciating: without such a mechanism, publishing the code would be almost meaningless for its most discussed part — the weights. You would see the structure and not see the numbers.

What this mechanism still does not give

A default is the main value, not the only one. At any moment part of the traffic is in experiments with other values. The stated policy: experiments running on a noticeable share of the traffic — roughly from ten percent — should be visible in the repository.

Hence the correct formulation of what you are reading in the code: «this is how it works for most users right now», not «this is how it works». The difference is substantial if you are trying to explain the behaviour of a particular person's particular feed.

And one more thing: the synchronisation goes on a schedule rather than instantly. Between a change in production and its appearance in the repository there is a lag.

3. Experiments through a stage's switch

We have already seen this mechanism on the page about the pipeline: every stage has an enable(query) method. Now it is clear why it receives the request — to read the parameters assigned to this particular user.

fn enable(&self, query: &ScoredPostsQuery) -> bool {
    query.params.get(EnableBidirectionalFollowHydration)
}

A fragment of BIDIRECTIONAL_BOOST_CHANGE.md · code by X, Apache 2.0, commit 28e414f

From that follows an elegant property: any stage can be experimented with, changing nothing in its code. A new source of candidates, a new filter, a new scorer — all of it is switched on for a percentage of the traffic by one parameter.

And since the wrapper around a stage counts metrics automatically, the measurement comes for free along with the experiment: how many times the stage ran, how many candidates it added or threw out, how long it took.

4. The history of one parameter day by day

The repository holds a separate document going through a real change — the very one widely discussed in the summer of 2026. It is a rare chance to see not the result but the process.

The substance of the change: posts from people you have a mutual follow with get a boost to the weight of the predicted probability of a reply. That is, if you and the author follow each other, the probability of your replying to their post enters the score with a raised coefficient.

DateWhat happened
10 July 2026An A/B test was launched: a small share of users were randomly assigned boost values of 5, 10, 15 or 20. For the majority the value is 0, that is, the mechanism is off. In parallel a second boost is being tested — to the weight of dwell time
13 July 2026On the first results the value 20 was rolled out to many users. The experiments with 0, 5, 10 and 15 continue on other shares
24 July 2026Following the experiments and user feedback the value was lowered to 15

Let us check how it ended by the code itself. In param.rs right now:

param!(
    BidirectionalFollowReplyWeightBoost, f64,
    "rust_home_mixer_bidirectional_follow_reply_weight_boost", 15.0
);
param!(
    BidirectionalFollowDwellWeightBoost, f64,
    "rust_home_mixer_bidirectional_follow_dwell_weight_boost", 0.0
);

A fragment of param.rs · code by X, Apache 2.0, commit 28e414f

The first value is 15.0, exactly what the story ended with. The second is 0.0: the boost to the weight of dwell time was tested in the same experiment and, as the document says outright, did not roll out widely. A zero parameter in the code is the trace of a change that did not happen, not a forgotten line.

What is genuinely instructive here

Not the fact of the experiment but the reason for the final decision. The value was lowered from 20 to 15 not because the metrics got worse but on the balance of things: the results of the experiments plus complaints from users that during the World Cup they were seeing too little discussion — because many relevant posts were written by accounts they do not follow.

Let us take the mechanics of that complaint apart, they are elegant. The boost strengthens mutual follows. By strengthening them, we automatically weaken everything else — including posts from unfamiliar accounts that at that moment are discussing an event of global scale. The engagement metrics could have stayed excellent all the while: people were actively talking with people they know. The problem was not in engagement but in the feed ceasing to perform the function of «showing what is happening in the world».

This is exactly the story about proxy metrics: the optimised quantity grows while the product gets worse along a dimension that is in no metric at all. And exactly the argument about diversity, that offline metrics always vote against it.

Note also the size of the boost — 15 against a base reply weight of 5.0, that is, for mutual follows the weight of a reply grows fourfold. That is not a fine adjustment but a very strong intervention, and the fact that it was calibrated publicly and step by step is telling in itself.

Common mistakes and hidden rocks

What people trip over
  • Reading a default as «this is how it works for everyone». It is the main value; part of the traffic is always in experiments with others.
  • Thinking that open code shows the current state in real time. The synchronisation of the defaults runs on a schedule, and there is a lag.
  • Forgetting that parameters bypass code review. 191 parameters are 191 ways of changing the behaviour of the product while skipping the usual process for reviewing changes.
  • Judging a change only by engagement metrics. In the case we looked at the metrics were good while the feed stopped performing its function — that came out of feedback, not out of a dashboard.
  • Not noticing zero parameters. A zero in the code often means «we tried it and it did not roll out» rather than «we forgot to remove it».

Interview questions

How do you organise the configuration of a recommender system so that experimenting is possible?

Everything that may become the subject of an experiment is moved into parameters read from an external system per request rather than globally. Then two simultaneous requests may have different values of one parameter, and that gives the split into groups with no branching in the code.

Second: every stage of the pipeline gets a switch that reads a parameter. That makes it possible to experiment not only with values but with the presence of a stage — a new source or filter is switched on for a percentage of the traffic.

Third, and often forgotten: the measurements have to be taken at the level of the framework, otherwise along with every experiment you will have to add the measurement of its effect by hand.

The price of the approach is that the discipline shifts from the code into the process of working with the configuration: a change to the product's behaviour no longer passes code review.

If the values live in an external configuration, what is the point of open code?

On its own — little: you would see the structure and not the numbers. That is why the repository we looked at states separately that the defaults are brought to the main production values by a script on a schedule.

It is important to understand correctly what that gives. The formulation «this is how it works for most users as of the last synchronisation» is true. The formulation «this is how it works» is not: part of the traffic is in experiments, and between a change in production and its appearance in the code there is a lag.

Tell us about a case where good metrics hid a problem.

A good example is right in this repository. In the summer of 2026 a boost was introduced to the weight of the probability of a reply for posts from people with a mutual follow: first a value of 20, then lowered to 15.

The reason for the reduction was not the metrics. Judging by the description they were good: people were talking with people they know more actively. The problem surfaced from feedback: during the World Cup users started seeing too little discussion of the event, because many relevant posts were written by accounts they do not follow, and strengthening mutual follows automatically weakened everything else.

The moral is exactly the one from the conversation about proxy metrics: the optimised quantity was growing while the product's function — to show what is happening in the world — was degrading, and no engagement metric showed that.

How do you tell whether a parameter sitting at zero is switched off or broken?

A zero usually means one of three things, and telling them apart matters. The mechanism was tested and did not roll out — that is the case in the repository we looked at with the boost to the weight of dwell time: it was tested in the same experiment as the boost to replies but was not rolled out widely. The head is trained but does not enter the score — as with a click on a profile: the prediction is computed, the weight is zero. The mechanism is waiting to be switched on — as with the inventory holdout with its percentages at zero.

What the three cases have in common is that a zero is the position of a knob, not the absence of a knob. The code of the mechanism is written, tested and ready; tomorrow the value may become non-zero without a single line of change.

One-screen cheat sheet

The declaration

param!: name · type · configuration key · default. Read per request, not globally.

How many

191 parameters — 191 ways to change behaviour without shipping.

The truth of the defaults

A scheduled script brings them to the main production values.

What a default does not mean

Not «this is how it is for everyone»: part of the traffic is in experiments. Plus the synchronisation lag.

Experiments

Through a stage's switch that reads a parameter. Works for values and for the presence of a stage alike.

The case

The mutual-follow boost: on 10 July a test of 5/10/15/20 → on 13 July 20 rolled out → on 24 July lowered to 15.

Why it was lowered

Not the metrics but feedback: strengthening mutual follows weakened everything else.

The scale of the intervention

A boost of 15 against a base reply weight of 5.0 — the weight grows fourfold.

Zeros in the config

Usually «tried it and it did not roll out» or «the head is trained but does not enter the score».

Primary sources