Errors and limits
HTTP status codes, concurrency quota, and request input bounds.
When something fails, start here. A 400 will never succeed unchanged; a 429
means wait and retry; a sync 504 means poll the job id — do not resubmit.
| Code | Meaning |
|---|---|
| 400 | Validation — unknown catalog agent, bad options, or a task_archive_url the platform will not fetch |
| 401 | Missing or invalid API key / Bearer token |
| 403 | Authenticated, but not permitted (wrong project, or provisioning without admin key) |
| 413 | CodeEdit payload too large |
| 429 | Project concurrency quota exhausted — honour Retry-After |
| 502 | The run reached the platform and failed inside it |
| 503 | Upstream or credential plane unavailable |
| 504 | Sync run/datapoint outlasted its wait — the run keeps going; job id is in headers |
Nothing on the platform answers a timeout with a 500. If you see one, that is a
bug worth reporting rather than something to retry around.
Long-running requests and who times out first
POST /v2/run/datapoint holds the connection open while your run happens. Each
layer waits longer than the one inside it:
| Layer | Budget | On expiry |
|---|---|---|
| Harbor's own wait | 1500s | 504 naming the job id |
| Gateway | 1560s for run/datapoint, 120s elsewhere, unbounded for SSE | 504 (run not cancelled) |
| Load balancer | 1800s | connection closed with no body |
If your runs take minutes, use POST /v2/jobs/execute-tasks. It returns a job
id immediately. The synchronous route is a convenience for short runs only.
A 504 is never a signal to resubmit — that buys a second run competing for the
same concurrency quota.
Quota is concurrency, not request rate
The limit is on how many jobs a project runs at once, not on how often you may submit. There is no requests-per-second throttle on the v2 routes, so do not expect one to protect you from a runaway loop.
The effective limit is resolved server-side for the authenticated
tenant:project: exact project policy, then tenant default, then the environment
default. A caller cannot raise its own limit with a request field. The operator
also sets a maximum configurable ceiling so one project cannot be provisioned
beyond the fleet and trial namespace capacity.
Read your current policy and usage:
curl -sS -H "X-Api-Key: $SANDBOX_API_KEY" \
"$SANDBOX_GATEWAY_URL/sandbox/v1/quota"The response includes max_concurrent, current_in_flight,
available_slots, and whether the value came from a project, tenant default, or
environment default. On shared dev the environment default and operator ceiling
are both 8.
What shares one counter today
GET /sandbox/v1/quota reports the main tenant:project bucket. These
surfaces bill that bucket while they are in flight:
| Surface | Counts toward GET /quota? |
|---|---|
| Harbor trials and standalone agent runs | Yes |
Cloud IDE (ide-ubuntu) and desktop sandboxes | Yes — one slot per running session |
Agent attach with sandbox_id | No extra slot (uses the session's slot) |
CodeEdit (POST /codeedit/v1/executions) | No — separate internal bucket (codeedit:{project}), not shown on GET /quota |
So an open Cloud IDE and a Harbor trial compete for the same max_concurrent
on the main bucket. Terminal CLIs inside the IDE (claude, codex, …) do not
take additional slots — only the session pod does.
Catalog profiles declare a quota_class (harbor, agent, codeedit,
sandboxes) for documentation; per-class limits and a multi-pool GET /quota
response are not implemented yet.
Over the limit on the main bucket:
| Surface | Behaviour when over quota |
|---|---|
POST /sandboxes/v1 (create/start IDE or desktop) | 429 when at cap |
POST /harbor/v2/run/datapoint (sync) | 429 with Retry-After |
POST /codeedit/v1/executions | 429 with Retry-After (CodeEdit bucket) |
POST /harbor/v2/jobs/execute-tasks (async) | Accepted with 202; the job waits in queued and the worker retries as capacity frees |
The async route deliberately does not reject: being over the cap is a transient capacity condition, and failing a submitted job for it would report "your job failed" when the truth is "your project was busy".
Running the platform yourself? The cap is enforced when the quota scheduler is
enabled (SANDBOX_L2_SCHEDULER=quota with SANDBOX_REDIS_URL set);
SANDBOX_PROJECT_MAX_CONCURRENT changes the fallback default and
SANDBOX_PROJECT_MAX_CONFIGURABLE bounds admin-provisioned policies. Raise the
ceiling only together with the worker fleet and trial quota — see
the worker elasticity runbook.
Bounds on a task archive
The platform fetches a URL you supply, so the fetch is bounded: 256 MiB downloaded, 1 GiB expanded, 20,000 entries. Full contract, including the presigned-URL rules: the task archive contract.
Harbor job failures
Poll GET /sandbox/harbor/v2/jobs/{job_id} — status: failed includes error detail.
Check worker logs for subprocess/Harbor CLI output.
A job can finish with status: succeeded and still contain a trial you must not
score — for example one trial of several raised AgentTimeoutError. Gate scoring
on outcome.valid_for_scoring, not on status. See
Reading the result.
An agent run reports completion rather than outcome, because nothing grades an
ad-hoc run: it tells you whether the agent finished, and makes no claim about scoring.
Desktop / viewer errors
| Status | Meaning |
|---|---|
409 | Sandbox not running yet — no viewer.url |
401 | Invalid or missing viewer token on the stream |
503 | Upstream stream (pod / Guacamole / Mac) not ready |
502 | Viewer proxy cannot reach the substrate |
Details: Viewer API.