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 SDK: try/catch misses a 402, generated calls return { error }
Generated @sume-com/sdk operations return { data, error, response } instead of throwing, so a 402 or 429 slips past try/catch. Check error on every call.
- Sume SDK error.retryable: server flag first, status only as fallback
SumeApiError.retryable uses the error envelope's retryable flag when present and falls back to 408, 429 or 5xx otherwise. A 409 is never retried by status.
- Sume STT language_code: set a hint or omit it for auto-detect?
language_code is optional on Sume STT. Omit it to auto-detect, pass a BCP-47 hint like en or ko when you know the language. A quick way to choose.
Written by Sume