Errors are RFC 9457 problem documents with Content-Type: application/problem+json.
{
"type": "https://docs.workspaces.build/api/errors#quota_exceeded",
"title": "Concurrency quota exceeded",
"status": 429,
"detail": "Project proj_7fTn21 is at its tier 2 concurrency limit of 50.",
"code": "quota_exceeded",
"request_id": "req_9Km2Xp",
"retry_after_s": 12,
"meta": { "tier": 2, "limit": 50, "current": 50 }
}
| Field | Stability | Use |
|---|
code | Stable | Branch on this |
status | Stable | HTTP status |
type | Stable | Link to this page's anchor |
title | May change | Human-readable summary |
detail | May change | Human-readable specifics — never parse |
request_id | — | Quote in support requests |
retry_after_s | Present when retryable | Seconds to wait |
meta | Additive | Structured specifics per code |
Branch on code. Parsing detail will break, because detail is written for a human and is
rewritten whenever a clearer phrasing exists.
Status codes
| Status | Meaning |
|---|
| 400 | Malformed or invalid request |
| 401 | Not authenticated |
| 402 | Payment or credit problem |
| 403 | Authenticated, not permitted |
| 404 | Not found, or not visible to this principal |
| 409 | Conflicts with current state |
| 422 | Well-formed but semantically invalid |
| 429 | Rate limited or quota exceeded |
| 500 | Platform error |
| 503 | Temporarily unavailable — capacity or degraded dependency |
| 504 | Upstream timeout |
Authentication and authorization
| Code | Status | Retry | Meaning |
|---|
unauthenticated | 401 | No | Missing or malformed Authorization |
invalid_credentials | 401 | No | Unknown, revoked or expired key |
token_expired | 401 | No | Session token past expires_at; mint a new one |
insufficient_scope | 403 | No | Key lacks the required scope; meta.required_scope names it |
account_suspended | 403 | No | Organization suspended |
not_found | 404 | No | Does not exist, or belongs to another project |
Quota, limits and spend
| Code | Status | Retry | Meaning |
|---|
rate_limited | 429 | Yes, after retry_after_s | Request rate ceiling |
quota_exceeded | 429 | Yes, when something finishes | Concurrency ceiling for the tier |
end_user_quota_exceeded | 429 | Yes | This end user's concurrency ceiling |
spend_cap_reached | 402 | No | Organization monthly cap |
end_user_cap_reached | 402 | No | This end user's spend cap |
insufficient_credits | 402 | No | Prepaid balance exhausted |
quota_exceeded is retryable because a concurrent session will finish. spend_cap_reached is not —
retrying cannot change the answer until a human raises the cap or adds credits.
Session and tier
| Code | Status | Retry | Meaning |
|---|
tier_capability_required | 409 | No | The operation needs a higher tier; meta.required_tier names it |
tier_unavailable_in_region | 422 | No | That tier is not offered in the requested region |
invalid_state_transition | 409 | No | e.g. resuming a session that is not paused |
session_not_running | 409 | Sometimes | Still provisioning — retry; terminal — do not |
session_terminated | 409 | No | meta.reason carries the terminal reason |
provision_timeout | 503 | Yes | Start exceeded the deadline; counts against the availability SLO |
capacity_unavailable | 503 | Yes, with backoff, possibly in another region | No capacity for this tier and size |
template_not_found | 422 | No | Unknown template or digest |
template_not_ready | 409 | Yes | Still building |
Execution
| Code | Status | Retry | Meaning |
|---|
run_timeout | 504 | Maybe | timeout_ms reached; the process was killed |
run_cancelled | 409 | No | Cancelled by the caller or by the session ending |
oom_killed | 500 | Yes, with more memory | Guest ran out of memory |
command_not_allowed | 403 | No | Blocked by the CLI allowlist; meta.binary and meta.rule |
output_too_large | 413 | No | Output exceeded the inline cap; read it from the filesystem instead |
Network and policy
| Code | Status | Retry | Meaning |
|---|
egress_denied | 403 | No | Destination not allowed; meta.destination and meta.rule |
policy_violation | 403 | No | Session killed by policy; meta.rule |
egress_quota_exceeded | 429 | No | Session bandwidth ceiling reached |
port_not_listening | 409 | Yes | Nothing bound to the port yet |
public_exposure_disabled | 403 | No | Organization has not enabled public ports |
Storage
| Code | Status | Retry | Meaning |
|---|
volume_in_use | 409 | Yes, after detach | Already attached to another session |
volume_zone_mismatch | 422 | No | Volume is in another zone |
snapshot_not_ready | 409 | Yes | Still uploading |
snapshot_tier_mismatch | 422 | No | Snapshots restore only at their own tier |
storage_quota_exceeded | 429 | No | Project storage ceiling |
residency_violation | 403 | No | Would move data out of an allowed region |
Request validation
| Code | Status | Meaning |
|---|
invalid_request | 400 | Malformed JSON or bad types; meta.errors is a field list |
missing_required_field | 400 | meta.field names it |
invalid_field_value | 422 | Well-formed, out of range; meta.field, meta.constraint |
idempotency_key_reused | 409 | Same key, different body |
idempotency_request_in_flight | 409 | Retry shortly — the original is still running |
unsupported_version | 400 | Unknown WB-Version |
| Code | Status | Retry | Meaning |
|---|
internal_error | 500 | Yes | Report with request_id |
service_unavailable | 503 | Yes | Dependency degraded |
upstream_timeout | 504 | Yes | Internal timeout |
Retry policy
const RETRYABLE = new Set([
'rate_limited',
'quota_exceeded',
'provision_timeout',
'capacity_unavailable',
'template_not_ready',
'snapshot_not_ready',
'port_not_listening',
'internal_error',
'service_unavailable',
'upstream_timeout',
])
async function call<T>(fn: () => Promise<T>, attempts = 5): Promise<T> {
for (let i = 0; i < attempts; i++) {
try {
return await fn()
} catch (e) {
if (!RETRYABLE.has(e.code) || i === attempts - 1) throw e
const wait = e.retry_after_s != null
? e.retry_after_s * 1000
: Math.min(2 ** i * 200 + Math.random() * 200, 30_000)
await sleep(wait)
}
}
throw new Error('unreachable')
}
Rules:
- Honour
retry_after_s when present. Ignoring it on rate_limited gets the key throttled
harder.
- Always send
Idempotency-Key on retried creates. Without it a retried POST /sessions whose
response was lost creates and bills a second session.
- Jitter the backoff. Unjittered retries from a fleet of agents synchronise into a thundering
herd.
- Never retry a 4xx that is not in the list.
spend_cap_reached, insufficient_scope and
tier_capability_required will return the same answer forever.
- Cap total retry time. An agent loop silently retrying for ten minutes looks like a hang.
Partial failures in streams
A stream that fails mid-flight sends a terminal error frame rather than closing the connection:
event: error
data: {"code":"oom_killed","detail":"Guest exceeded 4096 MiB","request_id":"req_9Km2Xp"}
Treat an SSE stream that ends without either an exit frame or an error frame as a transport
failure. Reconnect with Last-Event-Id to resume from where it stopped rather than re-running the
command.