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>(orX-WR-API-Key, orx-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 rotatedmeans the key was rotated and the old value’s grace period has ended. Use the new value.token_revokedmeans 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.
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:
-
Fix the cause: correct or rotate the upstream key, or check the host’s
baseURLforupstream_unreachable. -
Read a key’s breaker state with
GET /api/host-keys/by-id/{id}/health(state,reason,cooldown_remaining_seconds). -
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 matchingsecret_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’sadapter 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
adminpassword 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
adminwithRELAY_ADMIN_PASSWORDfrom.env. - Lean image: there is no user until you create one. Use
RELAY_ADMIN_TOKENas a bearer token, or mount akind: Userfile intoRELAY_CONFIG_DIR; see Docker. - Over plain HTTP, set
RELAY_COOKIE_SECURE=false; otherwise the session cookie is markedSecureand the browser drops it. The standalone image already does. - The admin UI is on the control port (
RELAY_CONTROL_PORT,8081by default), not the inference port.
Credentials and catalog regenerate on every run
Each newdocker 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 needRELAY_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:-
Run
relay secret statuswith only the new key configured and noteundecryptableRows. A value’s id is the id of the host key it belongs to. -
While any value in the format from before data keys cannot be decrypted,
relay secret rewrapis refused, so delete the values inundecryptableRowsby hand:An id that starts withpayload-logging:holds payload-logging credentials: write them again in thepayload-loggingsettings section. Any other id with no host key is the inference-token signing key: clearsigningKeyin theauth:tokenssettings section so the next boot generates a new one (tokens signed by the old key stop verifying). -
Run
relay secret rewrap --to <new kek id>, where the new KEK id is the one inheldKeks. It lists every data key under the lost key as skipped, and new data keys are wrapped under the new key from then on. -
Run
relay secret rotate --all, so every scope gets a primary data key the new key opens. - 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). WhenRELAY_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).
- Set
RELAY_MASTER_KEYto the correct key andRELAY_MASTER_KEY_EXTRAto the wrong one, and roll every pod. The pods convert the old-format values within seconds; wait untillegacyRowsis 0. New data keys are still wrapped under the wrong key until the next step. - Run
relay secret rewrap --to <kek id of the correct key>(the first entry inheldKeks). Every data key is wrapped under the correct key and it becomes the target. Skip this step iftargetKekis empty or already the correct key. - When
relay secret statusshows every data key under the correct KEK, removeRELAY_MASTER_KEY_EXTRAand roll every pod. - 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 0rows, and destroy them, as in If the master key leaks.
Health checks
/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.