Split a Sume job's queue wait from its run time with job events
Read GET /v1/jobs/{id}/events and subtract timestamps: job.queued to job.started is queue wait, job.started to the terminal event is run time. Python sample.

Fetch GET /v1/jobs/{id}/events, find job.queued, job.started and the terminal event, and subtract their created_at timestamps. The first gap is queue wait, the second is run time. A long queue wait usually points at your plan's concurrency rather than the model.
Events to read
Each event has id, type, source, status, message, created_at and a sanitized data object. The documented types are below.
| Type | Marks |
|---|---|
job.created | Accepted by the API |
job.queued | Waiting for a slot |
job.started | A worker picked it up |
generation.submitted | Handed to the generation service |
job.completed / job.failed / job.canceled | Terminal |
webhook.delivery | A webhook attempt, not part of the run |
Python
Queue capacity per plan is max(3, concurrency * 5), and concurrency is 1 on Free, 4 on Pro, 8 on Startup and 20 on Scale or Enterprise. With a Pro plan, a fifth simultaneous job waits for a slot, so a long queue wait is expected.
import asyncio, json, os, sys, urllib.request
from datetime import datetime
def get_events(job_id):
req = urllib.request.Request(f"https://api.sume.com/v1/jobs/{job_id}/events",
headers={"x-api-key": os.environ["SUME_API_KEY"]})
with urllib.request.urlopen(req, timeout=30) as r:
return json.load(r)["data"]["events"]
def ts(e):
return datetime.fromisoformat(e["created_at"].replace("Z", "+00:00"))
async def main(job_id):
events = await asyncio.to_thread(get_events, job_id)
first = {}
for e in events:
first.setdefault(e["type"], e)
queued, started = first.get("job.queued"), first.get("job.started")
end = next((first[t] for t in ("job.completed", "job.failed", "job.canceled") if t in first), None)
if queued and started:
print("queue wait s:", (ts(started) - ts(queued)).total_seconds())
if started and end:
print("run time s:", (ts(end) - ts(started)).total_seconds())
if not (started and end):
print("job has not both started and finished:", sorted(first))
asyncio.run(main(sys.argv[1]))Caveats
Skip webhook.delivery events in this arithmetic. They record delivery attempts that can happen after the job is already terminal.
Missing events
A job canceled early may have no job.started, in which case only the queue wait prints, or nothing. See which stage a failed job stopped at for the failure side.
Sources
Related posts
More in Developers
- Export every Sume job to CSV with next_cursor pagination in Python
Loop GET /v1/jobs?limit=100 and pass next_cursor back as starting_after until it is absent, then write id, status and captured cost to CSV with csv.DictWriter.
- Sume MCP 256 KiB output budget: oversize error is not a failed job
Sume's MCP tool results are capped at 256 KiB serialized. An oversize result returns an explicit error instead of a cut-off value, and the job may be fine.
- Sume MCP wait_busy 429: a retryable isError, paid creates queue 20 s
A busy Sume MCP server returns an isError result with http_status 429, retryable. The paid-create limits behind it and how to back off without double billing.
- Sume MCP token lasts 3600 s with no refresh grant: plan for it
Sume's hosted MCP OAuth issues one-hour access tokens and only the authorization code grant. What a long agent run should do at minute 61.
Written by Sume