Zaroz
Units API guide

Events

Every unit transition and every scaling change is written to an append-only log, in the same database transaction as the change. Read it with a cursor.

curl

TerminalCode
# long-poll: returns as soon as an event after cursor 0 exists, or after 30 s curl -sS -G "$UNITS/events" -H "$AUTH" \ --data-urlencode "after=0" --data-urlencode "wait=30" --data-urlencode "limit=100" \ --data-urlencode "types=unit.ready,unit.exited,unit.destroyed"

TypeScript

TypeScriptCode
let after = 0; for (;;) { const { status, body } = await call( "GET", `${UNITS}/events?after=${after}&wait=30&types=unit.ready,unit.exited,unit.destroyed`, ); if (status === 410) { after = body.resume_after; continue; } // cursor_expired for (const e of body.events) { console.log(e.seq, e.type, e.unit?.key, e.data); // persist e.seq next to your own state: it is the dedupe key } after = body.next_after; }

Python

Code
after = 0 while True: status, body = call( "GET", f"{UNITS}/events", params={"after": after, "wait": 30, "types": "unit.ready,unit.exited,unit.destroyed"}, ) if status == 410: # cursor-expired after = body["resume_after"] continue for e in body["events"]: print(e["seq"], e["type"], (e["unit"] or {}).get("key"), e["data"]) # persist e["seq"] next to your own state: it is the dedupe key after = body["next_after"]

The envelope (contract v1)

JSONCode
{ "seq": 1042, "id": "evt_0e8b5c6a-6f43-4b6b-8d36-6c9c1f2ad711", "type": "unit.exited", "contract_version": 1, "occurred_at": "2026-10-14T10:31:07Z", "service_instance_id": "6c9d...", "unit": {"id": "3f2a...", "key": "job-81", "labels": {"queue": "render"}}, "data": {"code": 0, "restarted": false} }

seq is gapless and strictly increasing per Workload, so a cursor never skips an event that commits late. Consumers must ignore unknown fields and unknown event types: additions are never a new version.

Typedata
unit.created, unit.starting, unit.ready, unit.stopped, unit.idle, unit.busy, unit.draining
unit.accepting, unit.not_accepting
unit.allocated, unit.allocation_delivered, unit.allocation_undelivered
unit.exited{code, restarted}
unit.failed{reason}
unit.destroyed{reason} (expired when destroyed after being stopped too long)
units.scaled{from, to, reason}; unit is null
rollout.started, rollout.completed, rollout.pausedunit is null
webhook.ping (webhook deliveries only)
events.gap (webhook deliveries only){from_seq, to_seq}: events a re-enabled webhook missed because they were no longer kept

unit.ready and unit.allocated carry the unit's private name and public address, so a matchmaker needs no second read. idle and busy are emitted on transitions only.

Parameters

ParameterMeaning
afterthe last seq you saw; 0 starts at the oldest kept event
typescomma-separated list to keep
limitup to 500
wait0 to 30 seconds: with nothing new, hold the request and return the moment an event arrives

The response is {events, next_after, oldest_retained_seq}. Send next_after as the next after. Events are kept 30 days. A cursor older than that answers 410 cursor_expired with resume_after.

Delivery is at-least-once

Persist the last seq you processed in the same transaction as your own effect, and treat a repeated seq as already done. This is the same rule that makes webhooks safe.

Last modified on