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.

4 min readSume
All posts

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.

POST /v1/jobs/{id}/cancel outcomes (Sume docs, read 2026-10-05)
ResponseMeaningSweeper action
200, status canceledCancelled before generation startedRecord it, reservation released
200, already canceledIdempotent repeatNothing
409 job_generation_already_startedGeneration began; cancelable is falseLeave it, keep polling
404Unknown or foreign jobDrop 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

All Developers posts

Written by Sume