Errors & rate limits
Pendra returns standard HTTP status codes. Errors come back as JSON
with a detail field describing the problem (see
error body shape), and inference responses carry an
X-Request-Id header you can paste into a support ticket.
Status codes
| Code | Meaning | What to do |
|---|---|---|
400 |
Bad request — malformed JSON, an invalid field, a prompt that's too long for the model's context window, or an encrypted request whose envelope is malformed (invalid_encrypted_request). |
Fix the request body. The detail / error.message field describes the problem (e.g. prompt exceeds context window). |
413 / 422 |
The request was rejected as too large or unprocessable — e.g. an audio upload over 25 MB, or an input over the model's limit. | Shorten the input or split it into smaller requests. The error message names the limit that was exceeded. |
401 |
Missing or invalid API key. | Check the Authorization or x-api-key header. Rotate the key from the console if needed. |
403 |
Authenticated but not allowed — e.g. owner-only operation called by a member, or an API key that's missing a required permission (GPU rental, for example, is off by default on new keys). An API key sent to an endpoint that only works from the signed-in console, such as managing API keys or billing, also gets a 403 saying so. That response doesn't check the key itself, so test the key on an endpoint it's allowed to use, such as /models. On inference endpoints it can also be a private inference refusal: your organisation has Require E2E turned on and the request was sent unencrypted (e2e_required), or an encrypted request came from an organisation whose plan doesn't include private inference (plan_required). |
The detail message names what's missing. Use a key with the right permission, from a user with the right role. For a private inference refusal, check detail.error.type and see private inference errors. |
404 |
Unknown route; the requested model isn't installed on any connected worker; it is installed but no worker will serve it (the hardware can't run it, or you switched Serving off for it); or the model doesn't do what this endpoint asks — for example a reranking model sent to /chat/completions. |
Read the message: it tells you which of those it is, and only asks you to install the model when the model really is missing. If it's installed but unusable it names the block — Model 'gemma4:31b' is installed on a connected worker, but its weights don't fit that worker's GPU memory… — and says re-installing won't change it; see model size. For a mismatch it names the model's actual job and the endpoint to call instead (Model 'bge-reranker-v2-m3' does not support chat completions — it is a rerank model. Use POST /api/v1/rerank instead.). Otherwise check the URL prefix (/api/v1 vs /v1). Browse available models. |
409 |
On an image generation request, or a chat request that includes an image: the model isn't fully installed on the worker that picked up the request — one of its files is missing or still downloading. Rare, because a model only takes requests once its install has finished — you'd see it if a file was removed from the worker afterwards, or while a model is being installed again. The message names what's missing, for example … is missing its text encoder (llm) (still downloading, or the install was interrupted) for an image model or … is missing its vision projector (still downloading, or the install was interrupted) for a chat model. The worker stays online and keeps serving your other models. On POST /v1/moderations, a 409 means no online worker has the moderation model you named (moderation_unavailable, see moderation errors). |
Retry once the install finishes — the console shows its progress on the worker's page. If nothing is installing, install the model again; files already downloaded are kept. For a moderation 409, retrying won't help until the model is installed on a worker that's online and up to date. |
429 |
Rate limited, or every worker serving the model is busy. Pendra briefly holds your request while a short spike clears, so most bursts go through; if the model stays saturated it returns this promptly rather than holding your connection open. | Back off and retry — honour the Retry-After header (seconds), which is sized to how busy things are; otherwise use exponential jitter. Higher plans get higher limits; if you self-host, add workers to handle more concurrent requests. |
500 |
Unexpected server error. | Retry once; if it persists, send the X-Request-Id to support. |
501 |
The worker that picked up the request can't run this model, or this feature of it, at all — a lasting limitation, not a temporary fault. For example, an image model whose build can't read prompts (… has no text encoder this worker can use, so it cannot turn a prompt into an image), or image input on a model whose image support that worker doesn't have. The worker stays online and keeps serving your other models. |
Retrying the same request won't help. The message says what isn't supported: use a different model, or send the request to a worker with different hardware. When the limitation is already known, the console marks the model Known issue on the worker's page. |
503 |
Pendra can't take the request right now. Usually it means no worker is currently connected that can serve the model (none online) — distinct from 429, which means workers are online but busy. Rarely, Pendra itself is briefly saturated or unavailable; that case comes back with "type": "service_unavailable" and the message Service temporarily unavailable, please retry. A brief drop in the worker's connection — even partway through a streaming response that's already sending tokens — usually doesn't fail the request at all: Pendra waits briefly for the worker to reconnect — or, if no output had been sent yet, for another worker that serves the model — and carries on from where it left off, with nothing repeated or missing (the response just pauses for a few seconds). Only if no worker picks it up in time do you get this error, which names the worker — for example Worker 'wrk-1a2b3c4d': disconnected while handling this request…. Very rarely, Pendra can't deliver a response it has started; the message then reads Pendra could not relay this response. Retry the request., and it can arrive partway through a streaming response. Retrying is safe. |
Retry shortly, honouring the Retry-After header; Pendra routes to a worker as one comes online. Check the error.type to tell the two apart, and check your worker is running if you self-host. A worker that drops mid-request almost always reconnects in time on its own, so seeing this error means it didn't — retry, and if it keeps happening, check that worker's logs on its page in the console. |
502 |
Gateway error — Pendra couldn't get a usable response from any worker (the worker dropped the connection or returned something it couldn't parse). A worker that rejects your request for a specific reason returns that reason's own code (e.g. 400) instead. A rarer case: a piece of a response can be lost before it reaches you — for example when a worker reconnects after a brief drop mid-stream and can't pick back up cleanly — and the message says so explicitly: part of this response was lost on its way through Pendra. Retry the request. |
The X-Worker-Id / X-Worker-Name headers identify the worker. Retry; Pendra will pick a different worker — including for the "part of this response was lost" case, which is always safe to retry. |
504 |
Timeout — a single request can run up to ~30 minutes before Pendra gives up. Pendra keeps the connection alive while a slow model works, so you rarely hit this. | If you do see it, the model or worker is likely overloaded — retry, or split a very large request into smaller ones. |
507 |
The model is too large for the worker that picked up the request — it doesn't fit that GPU, or the GPU ran out of memory while running it. Most common for image generation at a large size, or a big model on a small GPU. You can also get this on a request that sends an image: reading an image needs extra memory on top of the model, so a worker can turn down image input while still answering text for the same model. The error message says when that's the case. | Request a smaller image size, pick a smaller model, or run the model on a worker with a larger GPU (if you self-host, free that worker's GPU by turning off other models on it, or add a bigger card). Usually retrying the same request on the same worker won't help — the exception is a worker that shares memory between CPU and GPU, such as a Mac, where a model that normally fits can be turned away because the machine is busy right now. The error message says when that's the case, and the request succeeds once memory is freed. If only image input is being refused, send the request without the image, or retry once the worker has freed memory. |
Error body shape
Most errors carry a plain-text detail:
{
"detail": "Invalid or expired token"
}
For OpenAI-compatible endpoints, Pendra also returns the OpenAI-shaped
error envelope when the worker produces one — e.g.
{ "error": { "message": "...", "type": "..." } }.
Private inference errors put that
same envelope inside detail, so there
detail is an object rather than a string:
{
"detail": {
"error": {
"message": "This organisation requires end-to-end encrypted requests. Use the private inference option in the Pendra Python or Node SDK.",
"type": "e2e_required"
}
}
}
To handle every error with one piece of code, read the message from
error.message, then detail.error.message, then
detail when it's a string — and branch on
error.type or detail.error.type rather than
matching the message text.
Private inference errors
These come from private inference,
on any inference endpoint, and use the nested shape above. Each has a
stable type you can branch on.
| Status | detail.error.type | Meaning | What to do |
|---|---|---|---|
403 |
e2e_required |
Your organisation has Require E2E turned on in the console, and this request was sent unencrypted. | Send it through the private inference option of the Pendra Python or Node SDK, or have an owner or operator turn Require E2E off to allow unencrypted requests again. The key and role are fine — this isn't a permission problem. |
403 |
plan_required |
Private inference is part of the Pro and Enterprise plans, and your organisation isn't on one. You get this on an encrypted request, or when the SDK fetches your encryption key. | Upgrade from the console's settings page (the message links to it), or send the request without encryption. |
400 |
invalid_encrypted_request |
The encrypted request is malformed or uses an envelope version Pendra doesn't support — e.g. malformed private inference envelope or unsupported private inference envelope version 3. |
Use the Pendra SDK to encrypt requests rather than building the envelope by hand, and update the SDK if the message names an unsupported version. |
An encrypted request sealed to a key none of your connected workers holds
is a 503 instead — see X-Pendra-Error-Code
below.
Moderation errors
POST /v1/moderations
returns the OpenAI-shaped envelope with a stable error.code
(also sent as the X-Pendra-Error-Code header), and repeats the
message in a plain-text detail:
| Status | error.code | Meaning | What to do |
|---|---|---|---|
400 |
model_not_moderation |
The model you named isn't a moderation model. | Use a model with the moderation capability, such as llama-guard3:1b. |
400 |
image_input_not_supported |
The request included an image, and the moderation model checks text only. | Send images to a moderation model that checks images, such as singguard:4b. |
400 |
encrypted_input_not_supported |
The request was end-to-end encrypted. Moderation doesn't accept private inference requests. | Send the request without end-to-end encryption. Organisations with Require E2E turned on can't use this endpoint yet. |
404 |
model_not_found |
The model id isn't in the catalogue. | Check the id against the model catalogue. |
409 |
moderation_unavailable |
No online worker has the moderation model: none is connected, it isn't installed, it's switched off or doesn't fit, or the only workers with it need updating to a newer worker version. With content filtering on an API key, an end-to-end encrypted chat request can get it too: no online, up-to-date worker has both the chat model and the moderation model, so none can check it. | Don't retry straight away: it won't change until you act. Install the model, bring a worker online, or update the worker, as the message says. |
429 |
moderation_capacity |
Every worker with the moderation model is busy. | Retry after the Retry-After header's number of seconds. |
502 |
moderation_invalid_verdict |
The moderation model replied with neither a safe nor an unsafe verdict. | Treat the content as unchecked. Retrying the same input usually gets the same reply. |
503 |
moderation_unavailable |
The worker running the check failed or timed out. | Retry after the Retry-After header's number of seconds. |
Validation errors
If the request body is missing a required field or has one of the wrong
type, you get a 422 with a readable message plus a machine-
readable errors list you can map onto your own form fields.
{
"detail": "Invalid request. messages: Field required",
"error": {
"message": "Invalid request. messages: Field required",
"type": "invalid_request_error"
},
"errors": [
{ "field": "messages", "message": "Field required", "type": "missing" }
]
}
detail repeats the message so the same handling works for
every error Pendra returns. field is the dotted path to the
offending value as you sent it — messages, or
messages.0.role for a problem inside an array element.
Streaming requests get the status codes above too: anything Pendra can
decide before it starts answering — an unknown model, a model no worker has,
a model that's installed but no worker will serve, a model that doesn't do
what the endpoint asks, no worker online at all, an encrypted request sealed
to a key none of your workers holds any more, or workers too old for the
endpoint — comes back as a normal HTTP error, with the same headers
(including Retry-After and X-Pendra-Error-Code) a
non-streaming call would get. Only a 429 for busy workers, or a
503 for a worker that goes offline while your request waits for
it, can land after a stream has started, and those arrive in the body; see
errors on a stream.
Errors on a very slow request
If a request runs long enough that Pendra has started keeping the
connection alive (about 80 seconds), the 200 and its headers
have already been sent by the time anything can go wrong. A failure after
that point still reaches you — as a 200 whose body is the same
error object streaming uses:
{"error": {"message": "All workers serving 'qwen3.5:2b' are at capacity. Retry shortly, or add additional workers to handle more concurrent requests.", "type": "rate_limit_exceeded", "code": 429, "retry_after": 7, "queue_depth": 9}}
code is the status the response would have had, and on a
capacity rejection retry_after and queue_depth
carry the same guidance the headers would have. So on long-running
requests, check whether the body has an error key rather than
trusting the HTTP status alone.
Helpful response headers
| Header | On which responses | Use |
|---|---|---|
Retry-After | 429 and 503 responses | How many seconds to wait before retrying. On a capacity 429 it's sized to how backed up the model is. Honour it instead of retrying immediately. (On a very slow request the response has already started, so this rides in the body instead — see below.) |
X-Pendra-Queue-Depth | Capacity 429 responses sent before the response has started | How many requests were already waiting for a free worker when yours was rejected — a small number is a brief blip, a large one is sustained overload. |
X-Pendra-Error-Code | Selected 503 responses on encrypted requests, and the moderation errors from POST /v1/moderations | A stable machine-readable code you can branch on instead of matching the message text. On an encrypted request it's no_private_inference_worker, meaning no connected worker holds the encryption key your request was sealed to. The usual cause is that your fleet re-keyed since your client last fetched the key — fetch it again and retry. On moderation requests it's one of the codes listed under moderation errors. |
X-Request-Id | Inference responses (chat, messages, responses, embeddings, reranking, moderations, images, transcription, speech), streaming included | Quote this on a support ticket so we can look up the exact request. |
X-Worker-Id | Non-streaming inference responses, and streamed chat, messages and responses replies when a worker is free straight away | Identifies which GPU worker served the request. JSON replies also carry it in the body as pendra.worker.id, including a very slow one that was still waiting for a worker when its headers were sent — see Which worker served the request. On a stream it names the worker the request was first sent to, and is left out if the request had to wait for a busy worker. |
X-Worker-Name | The same responses as X-Worker-Id | Human-readable worker name from the console. If the name has any character outside plain ASCII (accents, CJK, emoji), it's sent percent-encoded as UTF-8 with a UTF-8'' prefix, e.g. café arrives as UTF-8''caf%C3%A9. Strip the prefix and URL-decode it (decodeURIComponent in JavaScript, urllib.parse.unquote in Python). |
Rate limits
Pendra applies per-organisation rate limits scaled to your subscription
plan. The default is generous and covers most production workloads. When
you hit the limit you'll get a 429 with a detail
message describing the bucket; back off with jitter and retry.
A 429 can also mean every worker serving your model is busy
right now. Pendra holds your request for a moment while a short burst
clears, so those usually go through without you noticing. If the model
stays saturated, you get the 429 back quickly — rather than
after a long wait — with a Retry-After header sized to how
backed up things are; back off for that long (add a little jitter) and
retry. The X-Pendra-Queue-Depth header tells you how many
requests were already waiting, so you can tell a brief blip from sustained
overload. Adding workers (if you self-host) clears it.
If you expect to sustain very high throughput, contact sales to discuss a dedicated plan.
Idempotency & retries
All inference endpoints are safe to retry. Chat completions and image generation are non-deterministic, so a retry produces a fresh sample — duplicate billing is not a concern because retries only bill on success.