Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

HTTP API

The HTTP gateway exposes the Tasks service of the gRPC API as JSON, except the Watch stream, plus the read-only Admin calls, metrics and health probes. It listens on server.http_addr (default 127.0.0.1:7071) and serves HTTP/1.1 and HTTP/2 (h2c without TLS, ALPN with TLS). With [server.tls] configured it serves HTTPS and accepts client certificates on the same terms as the gRPC listener.

Remote workers cannot use HTTP; they need the gRPC worker stream. Recurring schedules are managed over gRPC (Admin.AddRecurring) or in the embedding runtime.

Conventions

  • Authentication. Send authorization: Bearer <token>, or connect with a client certificate mapped in [[auth.certs]]. The admin role passes every check. Without any [[auth.tokens]] or [[auth.certs]], authentication is off and every request is treated as admin; the server only starts that way on loopback addresses unless --insecure-no-auth is given. In that mode, unless --insecure-no-auth is given, a request whose Host is not a loopback name or address (localhost, *.localhost, any 127.x.x.x, [::1]) gets 403.
  • Pool scope. A token or certificate with pools = [...] may only submit into those pools. Tasks in other pools look like they do not exist (404), so their ids and keys do not leak. Submitting a key that is already used by a task in a pool outside the scope returns 409.
  • Tenant. Credentials with a tenant submit for that tenant: tasks without a tenant get it, and naming another tenant returns 403.
  • Request bodies must be JSON with content-type: application/json (otherwise 415) and at most limits.max_http_body_bytes (default 16 MiB, otherwise 413). Unknown fields are rejected with 400, so a typo such as "priorty" never passes silently.
  • Task ids are strings in every JSON response ("id": "17"), because they are 64-bit and JavaScript numbers lose precision above 2^53. Requests accept ids as strings or numbers.
  • Timestamps (*_unix_ns) are Unix nanoseconds, returned as strings for the same reason. Durations (*_s) are seconds as JSON numbers and must be finite and non-negative.
  • Every request has a time limit of limits.request_timeout_s (default 30 s). Every request to /v1/tasks (POST and GET) and to every path ending in /result gets limits.max_wait_s (default 300 s) on top. A request that runs out of time returns 504.
  • Only the leader serves. A standby (or a leader that is shutting down) answers every /v1 route and /metrics with 503 not the leader (standby or shutting down). Put the servers behind a load balancer that routes on /readyz.

Routes

Method and pathRoleDescription
POST /v1/tasksclientSubmit one task; optionally wait for its result.
GET /v1/tasksclientList tasks, live and finished, one page at a time.
POST /v1/tasks:batchclientSubmit up to max_tasks_per_batch tasks atomically.
GET /v1/tasks/{id}clientTask details by numeric id.
DELETE /v1/tasks/{id}clientCancel a task by id.
GET /v1/tasks/{id}/resultclientWait for a task's result by id.
GET /v1/tasks/by-key/{key}clientTask details by key.
DELETE /v1/tasks/by-key/{key}clientCancel a task by key.
GET /v1/tasks/by-key/{key}/resultclientWait for a task's result by key.
GET /v1/snapshotmetricsPer-pool state, quality signals and totals.
GET /v1/workersmetricsConnected remote workers.
GET /v1/recurringmetricsRecurring schedules.
GET /metricsmetricsPrometheus text exposition (metrics reference).
GET /healthznone200 ok while the process runs (liveness).
GET /readyznone200 ready on the leader, 503 standby otherwise (readiness).

{id} must be a decimal task id; anything else returns 400 and points to the by-key route. Keys are percent-decoded path segments: encode / as %2F (/v1/tasks/by-key/report%2F42).

When server.metrics_addr is set, a second listener serves /metrics, /healthz and /readyz without authentication, for a Prometheus scraper or kubelet on a private network. The authenticated /metrics on http_addr stays available.

Task object

The body of POST /v1/tasks and each element of a batch's tasks. Every field is optional; {} submits an empty payload to the default pool.

FieldTypeDefaultDescription
payloadany JSON–Payload, sent to the worker as JSON bytes with content type application/json.
payload_b64string–Binary payload, standard base64 (content type application/octet-stream). Mutually exclusive with payload.
content_typestringfrom the payloadOverrides the content type passed to the worker.
poolstring"default"Target pool. Must exist (404) and be allowed for the caller (403).
kindstring"default"Free-form label passed to the worker (at most max_kind_len).
priority"critical", "high", "normal", "low""normal"Priority class.
keystring–Unique key: deduplicates submissions and names the task for dependencies and the by-key routes (at most max_key_len). Keys of the form <prefix>@<UTC time, RFC 3339> (for example report@2026-01-01T03:00:00Z) are reserved for recurring occurrences and rejected with 400.
deadline_in_s / deadline_unix_nsnumber / integer–Deadline, relative or absolute. Raises urgency as it approaches.
start_in_s / start_unix_nsnumber / integer–Fixed start time, relative or absolute (fixed starts).
tolerance_snumber1How late a fixed start may begin; 0 is allowed. Requires start_in_s or start_unix_ns.
on_miss"run_late", "skip", "fail""run_late"What happens when a fixed start misses its window.
duration_hint_snumberpool's default_duration_sExpected run time, used for reservations and hold leads.
timeout_snumbernonePer-attempt timeout; the worker's lease is aborted when it expires.
retryobjectpool's [pools.retry]{"max_attempts": n, "initial_backoff_s", "multiplier", "max_backoff_s", "jitter"}. max_attempts is required; jitter defaults to true.
afterarray[]Tasks that must succeed first: strings are keys; numbers or {"id": "..."} are ids.
depsarray[]{"key": "..."} or {"id": "..."}, with optional "cond": "on_success" (default), "on_complete", "on_failure".
resourcesobject{}{"<resource>": units} acquired for each attempt. At most max_resources entries.
tagsobject{}String labels passed to the worker. At most max_tags; each key and each value at most max_tag_len. Keys starting with cronoq. are reserved.
tenantstring–Tenant, for pools that share capacity fairly (Fair sharing).
labelsobject{}Worker labels the task requires: it runs only on workers that registered with all of them (Worker labels). Same limits as tags.
waitboolfalsePOST /v1/tasks only: respond when the task has finished. Rejected in batches.
wait_timeout_snumberlimits.max_wait_sUpper bound for wait, capped at limits.max_wait_s.

Use either the relative or the absolute form of a time, not both. Strings must not contain NUL characters. Dependencies (after plus deps) are limited to max_deps.

POST /v1/tasks

curl -s localhost:7071/v1/tasks -H "authorization: Bearer $CLIENT_TOKEN" \
  -H 'content-type: application/json' \
  -d '{"pool": "default", "payload": {"prompt": "hi"}, "priority": "high", "key": "job-1"}'
{"id": "17", "duplicate": false}

duplicate is true when a task with the same key is already known; id is then the existing task's id and nothing new is enqueued. With a durable store (Postgres or SQLite), the response is sent after the task is stored.

With "wait": true the response also carries the result:

{"id": "17", "duplicate": false,
 "result": {"id": "17", "status": "succeeded", "output_available": true,
            "output": {"answer": 42}, "output_b64": "eyJhbnN3ZXIiOjQyfQ=="}}

If the task is still running when wait_timeout_s expires, the request fails with 504; the task keeps running and its result can be fetched with GET /v1/tasks/{id}/result.

POST /v1/tasks:batch

{"tasks": [
  {"key": "extract", "payload": {"doc": 1}},
  {"key": "summarise", "payload": {"doc": 1}, "after": ["extract"]}
]}
{"tasks": [{"id": "18", "duplicate": false}, {"id": "19", "duplicate": false}]}

The batch is admitted atomically: either every task is accepted, or none is and the error names the offending task. Dependencies may reference keys defined anywhere in the same batch, in any order. Results are in request order. At most limits.max_tasks_per_batch tasks (default 1000).

GET /v1/tasks/{id} and GET /v1/tasks/by-key/{key}

{"id": "17", "status": "running", "pool": "default", "key": "job-1", "kind": "default",
 "attempt": 1, "max_attempts": 3, "output_available": false,
 "created_unix_ns": "1760000000000000000", "started_unix_ns": "1760000000120000000"}
FieldPresentDescription
idalwaysTask id (string).
statusalwaysSee statuses.
poolalwaysPool name (empty if unknown).
attemptalwaysCurrent or last attempt number (0 before the first dispatch).
output_availablealwaystrue when the task succeeded and its output is still retained.
key, kindwhen setTask key and kind.
tenantwhen setTenant the task was submitted for.
prioritywhen known"critical", "high", "normal" or "low".
max_attemptswhen knownAttempt budget from the task's or the pool's retry policy.
created_unix_ns, started_unix_ns, finished_unix_nswhen knownLifecycle timestamps (strings).
messagewhen setLast error or status message.

id and status are authoritative; the other fields are best effort and come from the serving leader's bounded info cache (outputs.info_entries) or, with a durable store, from the stored task. Unknown tasks, tasks past retention, and tasks in pools the caller may not use return 404.

GET /v1/tasks

Lists tasks in id order, oldest first, as task objects like those of GET /v1/tasks/{id}:

GET /v1/tasks?pool=default,reports&status=running,failed&limit=50
{"tasks": [{"id": "17", "status": "running", "pool": "default", "attempt": 1, ...}],
 "next_after": "17"}
Query parameterDefaultDescription
poolevery pool the caller may useComma-separated pool names. Unknown pools return 404, pools the caller may not use 403.
statusanyComma-separated statuses.
after–Only tasks with a larger id: pass the previous page's next_after.
limit100Page size, at most limits.max_list.

next_after is present when more tasks follow. With shards, the list covers every shard, and unavailable_shards names shards that could not be reached. It is present only when a shard was missing, on this route and on /v1/workers, /v1/recurring, the task-detail routes and the result routes; /v1/snapshot always includes it (empty when every shard answered). Live tasks come from the scheduler. Finished tasks come from the store, or, without one, from the bounded info cache, so only the most recent ones are listed. A page is not an atomic snapshot: a task that finishes while the page is built can be missing from it.

GET /v1/workers

{"workers": [{"id": "3", "name": "gpu-1", "pool": "default", "labels": {"gpu": "a100"},
              "slots": 4, "busy": 1, "connected_unix_ns": "1760000000000000000"}]}

Connected remote workers of the pools the caller may use, in connection order. id is assigned by the server per connection; name is what the worker registered with.

GET /v1/recurring

{"schedules": [{"name": "nightly", "cron": "0 0 3 * * *", "tz": "Europe/Berlin",
                "pool": "default", "kind": "report", "overlap": "queue",
                "next_unix_ns": "1760061600000000000", "last_task_id": "41"}]}

Recurring schedules of the pools the caller may use, by name. tz is absent for UTC, next_unix_ns when the schedule has no further occurrence, and last_task_id before the first occurrence.

DELETE /v1/tasks/{id} and DELETE /v1/tasks/by-key/{key}

{"cancelled": true}

false when the task already finished. Cancelling a running task aborts the worker's lease and cancels its handler. Dependents on an on_success or on_failure edge end as skipped; on_complete dependents proceed. With a durable store (Postgres or SQLite), the cancellation is stored before the response is sent.

GET /v1/tasks/{id}/result and GET /v1/tasks/by-key/{key}/result

Waits until the task finishes, at most timeout_s seconds (query parameter, default and maximum limits.max_wait_s). ?timeout_s=0 returns immediately.

curl -s "localhost:7071/v1/tasks/17/result?timeout_s=60" -H "authorization: Bearer $CLIENT_TOKEN"

Returns a result object, or 504 task is still running; wait again or poll. Results of finished tasks remain available while the leader's output cache (outputs.cache_entries, outputs.cache_bytes) or the durable store (Postgres or SQLite, store.keep_finished_s) still holds them.

Result object

{"id": "17", "status": "failed", "output_available": false, "message": "asked to fail"}
FieldPresentDescription
idalwaysTask id (string).
statusalwaysA terminal status.
output_availablealwaysfalse for a succeeded task whose output is no longer retained.
output_b64output availableThe output bytes, base64.
outputoutput is valid JSONThe output decoded as JSON.
messagewhen setWhy the task did not succeed.

GET /v1/snapshot

{"live_tasks": 12, "running": 4, "pending": 8, "workers_connected": 2, "unavailable_shards": [],
 "pools": [{"name": "default", "capacity": 4, "busy": 4, "held": 0, "desired": 6,
            "pending": 8, "pending_by_class": [0, 2, 6, 0], "reservations": 0,
            "dispatched": 120, "succeeded": 118, "failed": 1,
            "priority_adherence": 1.0, "fixed_start_hit_rate": 1.0,
            "utilization": 0.83, "schedule_efficiency": 0.80,
            "wait_p50_s": 0.2, "wait_p90_s": 1.4, "wait_p99_s": 2.9, "cost": 0.0,
            "workers_connected": 2,
            "groups": [{"labels": {}, "capacity": 3, "busy": 3},
                       {"labels": {"gpu": "a100"}, "capacity": 1, "busy": 1}]}],
 "totals": {"submitted": 130, "dispatched": 124, "succeeded": 118, "failed": 1,
            "dead_lettered": 0, "cancelled": 2, "skipped": 0, "missed": 0, "shed": 0,
            "retries": 4, "timeouts": 1, "lease_expiries": 0,
            "stale_completions": 0}}
FieldDescription
live_tasks, running, pendingTasks not yet finished; of those, running and waiting.
workers_connectedRemote worker streams across all pools.
unavailable_shardsShards that could not be reached; their part is missing from the response.
pools[].capacityWorker slots (sum of the slots of connected workers).
pools[].busy, held, desiredBusy slots; slots held idle for an upcoming fixed start; the autoscaler's target.
pools[].pending, pending_by_classWaiting tasks, total and by class (critical, high, normal, low).
pools[].reservationsFixed-start reservations not yet started.
pools[].dispatched, succeeded, failedAttempts started, tasks succeeded and attempts failed (including retried ones) within the metrics window (the last 60 s).
pools[].priority_adherence, fixed_start_hit_rate, utilization, schedule_efficiencyQuality signals in [0, 1] over the metrics window.
pools[].wait_p50_s, wait_p90_s, wait_p99_sTime from submission to first start, over the metrics window.
pools[].costCost since start (cost_per_worker_second × worker-seconds).
pools[].groupsWorker groups (workers with the same labels), unlabeled first: labels, slots and busy slots.
totalsLifetime counters of this leader since it started.

Field meanings: Metrics and quality signals.

Statuses

StatusTerminalMeaning
blockednoWaiting for dependencies.
schedulednoWaiting for its fixed start time.
readynoEligible and queued for a worker.
runningnoAn attempt is running on a worker.
retryingnoAn attempt failed; waiting for the retry backoff to elapse.
succeededyesFinished successfully.
failedyesFailed permanently, or missed a fixed start with on_miss = "fail".
dead_letteredyesRetryable failures used up every attempt.
cancelledyesCancelled by a caller.
skippedyesA dependency edge can no longer be met, an unknown dependency was awaited too long, or a recurring run overlapped the previous one.
missedyesMissed its fixed start with on_miss = "skip".
shedyesEvicted by a pool's shed_lowest / shed_oldest overflow policy.

Errors

Errors have the body {"error": "<message>", "code": "<reason>"}, where code is the lower-case HTTP reason phrase (for example too_many_requests). 401 responses carry www-authenticate: Bearer.

StatusgRPC equivalentCause
400INVALID_ARGUMENTMalformed JSON, unknown field or enum value, invalid duration, oversize string, dependency cycle, invalid id.
401UNAUTHENTICATEDMissing, malformed or unknown credentials.
403PERMISSION_DENIEDThe credentials lack the route's role, the pool or tenant is outside their scope, or authentication is off without --insecure-no-auth and the Host is not a loopback name or address.
404NOT_FOUNDUnknown task, pool, resource or dependency, or unknown route.
405–Method not allowed on this route.
409FAILED_PRECONDITION, ALREADY_EXISTSFixed-start reservation conflict; a key already used by a task in a pool outside the credentials' scope; a key that names tasks in several shards (use the id); a task or pool served by another shard that this server cannot forward to.
413–Body larger than limits.max_http_body_bytes.
415–Missing or wrong content-type.
429RESOURCE_EXHAUSTEDPool (max_pending) or server (max_pending_total) queue full. Retry with backoff.
500INTERNALUnexpected server error.
503UNAVAILABLENot the leader, shutting down, or the store is unavailable. Retry, preferably against the leader.
504DEADLINE_EXCEEDEDA wait ended before the task finished, or the request exceeded its time limit.

See also Errors for retry guidance.