A routing policy's strategy field was accepted, validated, and stored, then never read, so a policy set to cheapest sent ordinary traffic to the head of its fallback chain instead. The router now honours it, and a policy that could never be carried out is refused when you write it. Read the note on cost and latency ceilings: existing policies can start routing differently.
Your policy's `strategy` is now the control it looked like. A policy created with "strategy": "cheapest" and "is_default": true was accepted, checked against the seven-value enum, and written to the database, and then the router never looked at it. Requests that state no preference carry default, which sent them to the policy's rules and then straight down its fallback_chain, so the policy routed to the head of the chain, on every ordinary request, no matter which strategy it named.
The router now dispatches on the stored strategy, between the rules and the chain. cheapest, fastest, round_robin and weighted all take effect on traffic that states no preference of its own.
The order of precedence, which is worth knowing: a function_id on the request pins one function; a routing_preference of cheapest, fastest, round_robin or weighted is obeyed as sent and overrides your policy's strategy; otherwise your default policy decides, and within it a matching rule wins, then the strategy, then the fallback chain, then the single fallback function, and finally the same built-in-or-cheapest pick a workspace with no policy gets. The chain is now a fallback in fact as well as in name: it is reached when the strategy produces nothing available, such as a weighted policy whose weights name no function that is currently up.
A behaviour change, not only a fix: your cost and latency ceilings now bind. config.max_cost_per_unit and config.max_latency_ms are presented as maximums, and the router never read either of them. They are applied now, so a function above a ceiling is no longer a candidate and cheapest means the cheapest of the functions that qualify rather than the cheapest outright.
If you already have a policy carrying one of these, its routing can change the moment this ships, without you touching the policy. Two cases to check. Where some functions qualify, traffic can move to a different function than it used to reach. Where none qualify, the strategy now produces no pick at all and your fallback_chain decides, which for cheapest and fastest policies is the first time that chain has ever been reached: those two strategies always returned something before, so their chain was a fallback in name only. A ceiling you set once and forgot, or set tighter than any function can meet, is the case to look at first.
The ceilings constrain the strategy alone. A matching rule in config.rules names one function deliberately and still overrides them. Clearing a ceiling restores the previous behaviour exactly.
Policies that could never be honoured are rejected when you write them. weighted needs config.weights and policy needs config.rules; without them the strategy has nothing to act on and the request quietly fell through to the chain. Both are now a 400 on create and on update, and update checks the policy your patch produces rather than the patch alone, so setting "strategy": "weighted" on a policy that already carries weights still works. Update also validates its body at all, which it did not before: a misspelled strategy used to land in the column unchecked.
The routing policies screen keeps up with the new rules. Create and Save stay disabled while the form would build a policy the API refuses, and a rejected save now shows the reason in the dialog instead of leaving it open with nothing said. Editing a policy also no longer drops settings it was not showing you. The form builds its update from the policy it is editing rather than from the visible fields alone, so a cost ceiling, latency ceiling or other stored setting survives a save even when the selected strategy does not render a field for it. Clearing the single fallback function works again too.
Changing which policy is the default now takes effect immediately. Marking a new policy as default cleared the old one in the database but not in the running instance, so the superseded policy kept deciding routing until the instance restarted. Now that the strategy is read from the policy, that meant routing by a strategy you had replaced. The in-memory store follows the same one-default-per-workspace rule the database does.
A clearer error for a function you just registered. Registration creates a function as draft, and routing to one explicitly refuses anything that is not active, which is the first thing most integrations hit. The refusal now names the fix instead of only the state, quoting the PATCH that activates it. A function you deactivated yourself gets the refusal without that sentence.
One caveat, unchanged by this: a stored policy still only applies on the instance that created it, because nothing reloads policies from the database at start-up. If you need routing that holds on every instance, send cheapest, fastest or round_robin as the request's routing_preference; those three are computed from the available functions alone. weighted and policy still read their weights and rules from the stored policy, so they are not a workaround for this.
- A policy's
strategy is honoured for requests that state no preference - A
routing_preference of cheapest, fastest, round_robin or weighted still overrides the stored strategy weighted without a positive weight in config.weights, and policy without config.rules, are refused with a 400PATCH /api/routing-policies/{id} validates its body and can now answer 400- The policies dialog disables submit for a policy the API would refuse, and shows the reason when one is refused
- Editing a policy preserves its cost, latency and circuit breaker settings, and can clear the fallback function again
- A policy’s maximum cost per unit and maximum latency are now applied when the strategy picks a function
- Marking a new policy as default takes effect without waiting for a restart
- Routing to a
draft function explains how to activate it