Load balance routing strategy
Load balance spreads traffic across two or more targets by weighted random selection. It optimizes for distributing load across interchangeable providers β not for failover order (fallback), latency (least-latency), or cost (cost-optimized). Set strategy.mode: loadbalance to use it.
Split traffic between providers by weight β weight: 7 and weight: 3 means roughly 70/30 over time. A request whose chosen provider is unreachable, times out or is overloaded is handed to another provider in the pool; one the provider rejected as a bad request comes back as-is. weight: 0 stops new traffic to a provider without deleting it.
What happens to a requestβ
With OpenAI at weight: 7 and Anthropic at weight: 3, each request draws a starting provider β about seven in ten land on OpenAI, three on Anthropic. The split is a long-run ratio, not a strict rotation.
| The drawn provider⦠| The gateway⦠| The client gets |
|---|---|---|
answers 200 | returns it | that provider's answer |
returns 502 | asks the other provider | the other provider's answer |
| has an open circuit breaker | skips it before the call and uses the other one | the other provider's answer |
returns 400 (bad request) | stops | 400 |
Behaviourβ
loadbalance is a pool mode: after a failover-safe failure (a transport failure, an attempt that timed out waiting on the target, 408, 429, 5xx, an open circuit, or a full concurrency queue) the pipeline advances to the next target in the rotated order rather than reporting the failure back to the caller; any other 4xx is returned to the client. targets[].retry still governs how many times any one target is retried before the pipeline moves on.
On each request, LoadBalance.SelectTargets (internal/strategies/loadbalance.go) filters targets[] down to those whose provider is registered and whose SupportsModel(req.Model) returns true, then picks a weight-biased starting index (weightedStartIndex, internal/strategies/targetorder.go) and returns the full compatible list rotated from that index. The first key in the returned order is the weighted pick the pipeline commits to first; the rest stay available as substitutes the pipeline may use before committing β for example when the picked target's circuit is open. SelectTargets returns nil (the caller reports 404 model_not_found) when no compatible target exists, or when every compatible target has been drained to weight 0.
Config keysβ
| Key | Type | Default | Description |
|---|---|---|---|
strategy.mode | string | β | Set to loadbalance. |
targets[].weight | float64 | 0 if omitted (YAML omitempty) | This target's relative share of traffic. Read here and, for equal-cost ties, by cost-optimized; ignored by every other mode. 0 means the target receives zero traffic β it can never be the rotation's start index. |
strategy.sticky.on | string | β | Set to user to pin each request to the same start target for the same user field: a stateless hash, so a conversation keeps its provider prompt cache without any shared state, and every replica with this config answers the same. A request with no user draws at random. |
strategy.sticky.ttl | duration | none | Rotates pins: a user stays pinned for at most one window ("1h"), after which it may hash to another target. |
Sticky sessionsβ
strategy:
mode: loadbalance
sticky:
on: user # the request's `user` field
ttl: 1h # optional; a pin lasts at most one window
With sticky, every request carrying the same user starts on the same target, so a multi-turn conversation keeps hitting the provider that holds its prompt cache. The pin is a hash of the user, not a table: nothing is stored, nothing is shared between gateway replicas, and a request without a user is a normal weighted draw. sticky also applies on embeddings and image requests, which carry the field.
Minimal working YAMLβ
strategy:
mode: loadbalance
targets:
- virtual_key: openai
weight: 0.7
- virtual_key: anthropic
weight: 0.3
Interchangeable providers rarely share model IDs; give the name your clients send a translation on each target so the draw can land anywhere:
targets:
- virtual_key: openai
weight: 7
model_map:
smart: gpt-4o-mini
- virtual_key: anthropic
weight: 3
model_map:
smart: claude-sonnet-4-6
Every response still says "model": "smart". See One model name, different upstream IDs.
When to useβ
- Spreading load across two or more providers serving the same (or overlapping) models, for cost or capacity reasons.
- Gradually shifting traffic during a migration by adjusting weights across deploys.
- Draining a target to zero traffic before revoking its credential or removing it from
targets[].
Gotchasβ
- Weights are relative, not percentages.
weight: 0.7andweight: 0.3behave identically toweight: 7andweight: 3β only the ratio between targets matters. - Omitting
weightyields0, not an even split. Becauseweightuses YAMLomitempty, a target with noweightkey defaults to0.0β silent zero traffic. This is an easy foot-gun: setweightexplicitly on every target underloadbalance. - An all-zero or negative weight set is rejected at load.
ferrogw validate(and gateway startup) reject a negative weight outright and reject a config where every target's weight sums to zero β but one forgotten weight on an otherwise-valid config passes validation and silently drains just that target. - This is a pool mode. The pipeline advances past an open-circuit target, or one that failed in a failover-safe way, to the next one in the rotated order; any other
4xx(400,401,403,404,422, β¦) is returned to the client. Aloadbalanceconfig gets failover "for free" as a side effect of the mode family, not because of anything weight-specific. - Only model-compatible targets are eligible. A target whose provider isn't registered, or that doesn't serve the requested model, is filtered out before weighting is applied β it never wins the draw regardless of its configured weight.
- Sticky hashing changes the draw, not the pool.
sticky: { on: user }maps eachuserinto the same weight-proportional draw every time, so a pinned user still lands on a target in proportion to the weights; changing weights or the target set re-maps a share of users. Aweight: 0target is never pinned to. - Selection uses
math/rand, deliberately β this is a load-shaping decision, not a security-sensitive one.