Cancel video jobs still queued after 10 minutes: handle the 409
A Python sweeper that cancels Sume video jobs queued too long. POST /v1/jobs/{id}/cancel works only before generation starts; the 409 means keep waiting.

To cancel a Sume video job that has been queued too long, send POST /v1/jobs/{id}/cancel. It succeeds only before generation starts, and after that it returns 409 job_generation_already_started with details.cancelable false. The sweeper below cancels jobs older than ten minutes that still read pending, and leaves a started job alone, because that job will finish and bill.
Why you would cancel
Queue-first admission means a valid submit is accepted as queued while the workspace is at its concurrency limit. That is normal, but it can leave a request waiting well after it stopped mattering: a campaign was cancelled, a customer closed a tab, or a newer take replaced it. A queued job holds a reservation of your balance and a place in the accepted capacity, which is 24 jobs on a Pro plan.
Cancelling frees both. Sume documents that a cancel of a job that is already canceled is idempotent and returns the same canceled job, so the sweeper is safe to run on a timer without bookkeeping.
There is also a fairness point if several services share one workspace. The accepted capacity is per workspace, so one service's stale queue can crowd out another's fresh work. A sweeper that frees the abandoned slots is a cheap way to keep a shared workspace healthy without raising the plan.
What each answer means
The cancel route has few outcomes. Branch on them explicitly so that the sweeper never misreads a started job.
| Response | Meaning | Sweeper action |
|---|---|---|
| 200, status canceled | Cancelled before generation started | Record it, reservation released |
| 200, already canceled | Idempotent repeat | Nothing |
| 409 job_generation_already_started | Generation began; cancelable is false | Leave it, keep polling |
| 404 | Unknown or foreign job | Drop it from your list |
The sweeper
Keep a dict of job ids with their submit times, from your own store. The script reads each job through the video route and cancels the ones still pending after the cutoff.
import json, os, time, urllib.request, urllib.error
H = {"Authorization": "Bearer " + os.environ["SUME_API_KEY"], "Content-Type": "application/json"}
BASE = "https://api.sume.com"
def call(method, path):
req = urllib.request.Request(BASE + path, method=method, headers=H,
data=b"{}" if method == "POST" else None)
try:
with urllib.request.urlopen(req, timeout=30) as r:
return r.status, json.loads(r.read() or b"{}")
except urllib.error.HTTPError as e:
return e.code, json.loads(e.read() or b"{}")
def sweep(submitted: dict, max_queue_s: int = 600):
now = time.time()
for job_id, t0 in list(submitted.items()):
_, job = call("GET", f"/v1/videos/{job_id}")
if job.get("status") != "pending" or now - t0 < max_queue_s:
continue
code, body = call("POST", f"/v1/jobs/{job_id}/cancel")
err = (body.get("error") or {}).get("code")
print(job_id, code, err or "canceled")
if code == 200 or err != "job_generation_already_started":
submitted.pop(job_id, None)
sweep({"job_replace_me": time.time() - 3600})
What to expect
The status check uses pending because that is the video-route name for a queued job. If you read the generic jobs route instead, the word is queued. Do not cancel in_progress jobs. They are past the point where cancel can work, and asking only produces the 409.
A cancelled job releases its reservation, so your balance returns to what it was before the submit. A job that is canceled after generation started cannot happen, since the API refuses, and you pay for that completion like any other.
Policy tips
Pick the cutoff from your own business, not from a default. A live-event clip might be stale after two minutes, and an overnight batch might wait an hour. The 10 minutes in the code is an example you should change.
Pair the sweeper with a wave pacer so you rarely queue more than you can run. Then the sweeper catches only the unusual case, such as a provider slowdown, and not every job.
- Cancel through the jobs route even for a /v1/videos job. They share the same job id.
- Log the cancel result with the request_id.
- Do not retry a 409. Start polling again instead.
- Never resubmit a cancelled intent with the same Idempotency-Key and a changed body. Use a new key.
Sources
Related posts
More in Developers
- canceled vs cancelled: one letter that breaks video status checks
Sume native jobs say canceled and send job.canceled. The OpenRouter-shaped /v1/videos response says cancelled. Normalize the spelling before you branch.
- Sume 501 capability_not_configured: no job started, no credits spent
501 capability_not_configured means that feature is not connected on the platform. No job starts and nothing is charged. Do not retry; contact support.
- Caption 40 clips in six languages with no language hint: $8
Leave `language` off and Sume's caption job detects it. Forty clips of up to 60 seconds cost $8.00 at $0.20 each; here is the loop and the style trap.
- Caption cue limits: 400 characters, 200 cues, 60 seconds
A caption cue takes 1-400 characters, start of 0 or more, end above start and 60 s or less; a request holds 1-200 cues. Validate locally before the $0.20 job.
Written by Sume