Skip to main content
Start from the status code and the code field of the error body: {"error": {"type": "...", "code": "...", "message": "..."}}. Routing, rate-limit and upstream errors Relay writes itself carry X-WR-Origin: relay and an X-Should-Retry verdict. When the upstream provider rejected the request on every key Relay tried, Relay forwards the provider’s last response instead, with its status and body, X-WR-Origin: upstream, and the provider’s status in X-WR-Upstream-Status.

401: relay key or token rejected

The code is unauthenticated, and the message says why: missing relay key, invalid api key, api key disabled, api key revoked, api key expired, api key rotated, invalid token, token expired.
  • Send the relay key as Authorization: Bearer <relay-key> (or X-WR-API-Key, or x-api-key).
  • Use the full value returned when the key was created, not its id or prefix. It is shown once; if you lost it, create or rotate a key.
  • api key rotated means the key was rotated and the old value’s grace period has ended. Use the new value.
  • token_revoked means an inference token was revoked, by id (DELETE /api/auth/token/{jti}) or for all of a user’s tokens (POST /api/auth/token/revoke-all). Mint a new one; see Inference tokens.
A 401 with X-WR-Origin: upstream is the provider rejecting a host key, not your relay key; see Every key fails.

403: authenticated, but not allowed

404 model_not_found

The model value matches no catalog model or alias. List the models your credential can reach:

503: nothing to route to, or relay is overloaded

502: every key fails or is cooling down

Each host key has a circuit breaker. A key that the upstream rejects with 401 cools down for 30 seconds, then 1, 5 and 15 minutes, then an hour per further failure; 403 backs off from 1 to 60 seconds, and so do 5xx and network errors once 5 of them land within 30 seconds; a 429 with a Retry-After above 5 seconds cools it down for that long. A rotated key value starts with a fresh breaker. See Key pooling. To recover:
  1. Fix the cause: correct or rotate the upstream key, or check the host’s baseURL for upstream_unreachable.
  2. Read a key’s breaker state with GET /api/host-keys/by-id/{id}/health (state, reason, cooldown_remaining_seconds).
  3. Wait out the cooldown, or rotate the key’s value, which gets a fresh breaker. With RELAY_STATE_BACKEND=memory, restarting relay clears every breaker. With Valkey, breaker records are the keys matching secret_health:*; deleting them clears them:

429: slow down

A 429 written by Relay always means a limit set for the caller. Relay’s own lack of capacity is a 503 (see above).

413: request too large

request_too_large: the body is over RELAY_MAX_REQUEST_BYTES (32 MiB by default); see Configuration. In proxy mode request_too_large also means the first 32 MiB of the body hold no top-level model: Relay keeps the body until it reads the model, so send model before messages in large bodies.

408: request timeout

request_timeout (proxy mode): the client sent no request body bytes for RELAY_PROXY_BODY_IDLE_S seconds (30 by default) while the body streamed upstream; see Configuration.

Wrong wire format from the upstream

If a model returns a wire-format error from its provider, the host binding’s adapter probably doesn’t match how that endpoint speaks. Valid adapters are openai (Chat Completions and OpenAI-compatible servers such as Ollama), openai_responses, anthropic and gemini (Gemini’s native generateContent). The adapter is set per binding, not per host, because one host can serve models over different wire shapes.

The admin UI won’t sign you in

  • Standalone image: the admin password is printed to the container log on first boot (docker logs <container>) and saved on the data volume. On a volume that already had a config directory, no new password is generated; read it back as in Retrieving or pinning the master key.
  • Compose stack: sign in as admin with RELAY_ADMIN_PASSWORD from .env.
  • Lean image: there is no user until you create one. Use RELAY_ADMIN_TOKEN as a bearer token, or mount a kind: User file into RELAY_CONFIG_DIR; see Docker.
  • Over plain HTTP, set RELAY_COOKIE_SECURE=false; otherwise the session cookie is marked Secure and the browser drops it. The standalone image already does.
  • The admin UI is on the control port (RELAY_CONTROL_PORT, 8081 by default), not the inference port.

Credentials and catalog regenerate on every run

Each new docker run of the standalone image without a volume starts from an empty data directory and generates a new master key, new credentials and a fresh catalog. Mount a volume; see Persisting data.

Inference tokens can’t be minted

Relay keeps the token signing key as a stored secret, so tokens need RELAY_MASTER_KEY. Without one, relay logs inference tokens are disabled at startup and minting fails. Set the master key and restart.

Stored host key won’t decrypt after a restart

RELAY_MASTER_KEY changed between runs. It must be stable: it unwraps the data keys stored secrets are encrypted under. relay secret status lists those data keys as locked, and lists the ids of the values the process cannot decrypt in undecryptableRows. Restore the original master key, or keep the new one and add the original as RELAY_MASTER_KEY_EXTRA, then follow Changing the master key to move onto the new one. The standalone image keeps the key stable as long as you mount a volume.

The original master key is lost

The values encrypted under it are lost too. To store values again under the new key:
  1. Run relay secret status with only the new key configured and note undecryptableRows. A value’s id is the id of the host key it belongs to.
  2. While any value in the format from before data keys cannot be decrypted, relay secret rewrap is refused, so delete the values in undecryptableRows by hand:
    An id that starts with payload-logging: holds payload-logging credentials: write them again in the payload-logging settings section. Any other id with no host key is the inference-token signing key: clear signingKey in the auth:tokens settings section so the next boot generates a new one (tokens signed by the old key stop verifying).
  3. Run relay secret rewrap --to <new kek id>, where the new KEK id is the one in heldKeks. It lists every data key under the lost key as skipped, and new data keys are wrapped under the new key from then on.
  4. Run relay secret rotate --all, so every scope gets a primary data key the new key opens.
  5. Re-enter the values of the host keys from step 2 and of any other host key that loads unresolved.

Wrong master key at the upgrade to data keys

This applies to the upgrade to 0.11.6 (schema 34). When RELAY_MASTER_KEY held a different key than the one the stored values were encrypted under before the upgrade, those values stay in the old format: legacyRows stays above 0, undecryptableRows lists them, pods log cannot rewrite stored secret, and the host keys load unresolved. Values written since then (for example a host key value entered again) were stored under data keys wrapped by the wrong key, and the first of them made it the target (targetKek).
  1. Set RELAY_MASTER_KEY to the correct key and RELAY_MASTER_KEY_EXTRA to the wrong one, and roll every pod. The pods convert the old-format values within seconds; wait until legacyRows is 0. New data keys are still wrapped under the wrong key until the next step.
  2. Run relay secret rewrap --to <kek id of the correct key> (the first entry in heldKeks). Every data key is wrapped under the correct key and it becomes the target. Skip this step if targetKek is empty or already the correct key.
  3. When relay secret status shows every data key under the correct KEK, remove RELAY_MASTER_KEY_EXTRA and roll every pod.
  4. If the wrong key may be known elsewhere (it belongs to another deployment, or came from an example), whoever has it and a copy of the database from that period can unwrap the data keys it wrapped. Rotate every scope (relay secret rotate --all), wait until the old data keys show 0 rows, and destroy them, as in If the master key leaks.

Health checks

The control listener serves its own probes at /api/healthz (Postgres, and Valkey when RELAY_STATE_BACKEND=redis; not the catalog) and /api/livez.
When asking for help, include the request, the full error body with its X-WR-* response headers, and the output of /healthz and /api/version.