Skip to main content

Single routing strategy

Single routes every request to one target β€” targets[0] β€” and nothing else. It optimizes for simplicity and explicit control: no weight math, no latency tracking, no per-request selection logic, just a provider behind a governance and observability layer. It's also the strategy you get by not choosing one: strategy.mode: single, or an empty/omitted mode, both build this strategy.

In plain words

One provider, full stop. The gateway still checks keys, applies limits and plugins, and logs everything, but it never picks a different provider. If that provider fails, its failure is your answer β€” add retry to absorb brief blips.

What happens to a request​

With targets: [openai] and retry.attempts: 3:

OpenAI…The gateway…The client gets
answers 200returns itOpenAI's answer
returns 503 three timesretries twice with backoff, then gives up503
returns 401stops immediately β€” not a retryable status401
has an open circuit breakerstill attempts it; the breaker refuses503 upstream_unavailable
does not serve the requested modelnothing is attempted404 model_not_found, even if a second target in the list would serve it

Behaviour​

single is a named mode β€” the opposite of a pool mode like fallback or loadbalance. Single.SelectTargets (internal/strategies/single.go) returns a one-element slice built once at construction, []string{targets[0].VirtualKey}. It never looks at the request, never checks which model was asked for, and never sees targets[1:] at all β€” buildStrategy (gateway_strategy.go) constructs Single from targets[0] alone, so any additional entries in targets[] are configured but structurally invisible to this strategy. There is no failover: a failure at targets[0] β€” after targets[].retry exhausts its attempts against that same target β€” is the answer the caller gets.

An open circuit breaker on targets[0] behaves differently here than under a pool mode, and it's worth being precise about it. The pipeline's open-circuit filter (healthyKeys, gateway_pipeline.go) only filters when the strategy handed it more than one candidate to choose from β€” with a single-element list, it returns that element unfiltered, open or not. So under single, an open circuit does not divert traffic anywhere: the request is still attempted, the circuit breaker itself refuses the call, and the gateway answers 503 upstream_unavailable. That's a different outcome from fallback, where an open circuit on the lead target is passed over in favor of the next one in targets[] β€” single has no such list to fall back into. A conditional or content-based rule with one target behaves exactly like single here; a rule with a target_keys chain moves on to the next chain member.

Config keys​

KeyTypeDefaultDescription
strategy.modestringsingleSelects this strategy. Leaving mode empty resolves to single too (Config.Normalize) β€” it's the gateway's zero-value default.
targets[].virtual_keystringβ€” (required)Provider registration name. Only targets[0].virtual_key is ever read under this mode.
targets[0].retry.attemptsint1 (no retry) β€” omitted or <= 0 normalizes to 1Attempts against targets[0] before giving up. Resolved by the pipeline, not the strategy, so it applies here exactly as it does under every other mode.
targets[0].retry.on_status_codes[]inttransport errors + 408, 429, 5xxRestricts retries to these HTTP status codes; other 4xx codes are deterministic client errors and are never retried.
targets[0].retry.initial_backoff_msint100Base for full-jitter exponential backoff between attempts against targets[0].
targets[0].circuit_breaker.*objectno breaker unless set (failure_threshold: 5, success_threshold: 1, timeout: "30s" if configured)See the behaviour note above β€” with one candidate, an open circuit fails the request rather than routing around it.
targets[].weightfloat640Ignored under single. Only loadbalance reads it.

Minimal working YAML​

strategy:
mode: single

targets:
- virtual_key: openai
retry:
attempts: 3

When to use​

  • One provider, no routing decisions to make β€” the gateway is purely a governance/observability layer (auth, plugins, metrics, request logging) in front of a single upstream.
  • You want the simplest possible config while you evaluate the gateway, before adding a second provider.
  • You're intentionally pinning all traffic to one provider for compliance or contractual reasons, and any deviation should be a config change you make on purpose β€” not something the gateway decides for you.

Gotchas​

  • Only targets[0] counts. A second, third, or further entry in targets[] is accepted by ferrogw validate β€” with a startup warning naming the unused targets since v1.5.2 β€” but is never selected, never health-checked by the strategy, and never receives traffic. If you meant to fail over to it, use mode: fallback instead.
  • Readiness doesn't know that either. /readyz and startup logging check whether each configured target's provider is registered, not which one single would actually pick β€” so a config with a broken targets[0] and a perfectly healthy targets[1] still reports 200 ready. Put the target you mean to use first.
  • No failover, by design. A failure at targets[0] β€” once retry.attempts is exhausted β€” is the response the caller gets. Add retry to absorb transient errors against that one target; switch strategies if a hard outage should move traffic elsewhere.
  • An open circuit doesn't skip the target β€” it fails the request. Because SelectTargets always returns exactly one key, the pipeline's open-circuit filter has nothing to filter between and never fires. The call is attempted anyway, the breaker refuses it, and the caller gets 503 upstream_unavailable β€” even if another configured target could serve the model. This is easy to assume works like fallback's circuit handling; it doesn't.
  • weight is silently accepted and silently ignored. Setting targets[0].weight (or any other target's) does nothing under single β€” it only affects loadbalance.