Cancel a completed job: 409 job_not_cancelable vs already started

Sume returns 409 job_not_cancelable when you cancel a finished job, and job_generation_already_started mid-run. How to tell them apart and handle each.

5 min readSume
All posts

If POST /v1/jobs/:id/cancel returns 409 job_not_cancelable, the job is already terminal (for example completed), so there is nothing left to cancel; read the result instead. A different 409, job_generation_already_started, means the job is still running but generation work has begun, so it will finish or fail on its own and keeps billing.

Both are the conflict class: per MDN, 409 means the request conflicts with the current state of the target resource. Sume's cancel route only succeeds before generation starts, as the jobs and results page states.

What does job_not_cancelable look like?

The error envelope carries the usual request_id, plus details that say which state the job was in and when cancellation would have been possible. In the API's own test fixture for a completed job, the body has code: "job_not_cancelable", the message "API job can only be canceled before generation starts.", retryable: false, next_action: "inspect_events", and details: { status: "completed", cancelable_before: "generation_started" }.

Because retryable is false, a retry loop that treats every 409 as transient will spin on it forever. Treat it as an answer, not a failure.

How is it different from job_generation_already_started?

The errors page lists three job-state 409 codes: job_not_completed, job_not_cancelable, and job_generation_already_started. Only the last one means the job is alive.

In the docs, cancel after generation starts returns 409 job_generation_already_started with details.cancelable: false, and the job completes or fails normally. Read the status field of the job before deciding what to do next.

Cancel outcomes by job state (docs read 2026-10-02)
Job state when you cancelResponseWhat to do
queued, or processing before generation startsJob becomes canceledStop polling; reservation is released
processing, generation started409 job_generation_already_startedKeep polling to terminal
completed, failed409 job_not_cancelableRead the result or the error
already canceledSame canceled job returned (idempotent)Nothing, it is a no-op

What does cancelling an already canceled job return?

The docs call cancel on a canceled job idempotent: it returns the same canceled job. The OpenAPI schema for the cancel response includes an idempotency_hit boolean described as true when the job was already canceled before this request, which lets a retrying client tell a fresh cancel from a replay.

This matters for shutdown hooks that cancel everything they submitted: a second pass over the same ids is safe.

A cancel helper that handles all three

The sketch below treats job_not_cancelable as a normal outcome and routes the caller to the right next read.

Note that cancelling is a request for a refund of work that has not started. Once job_generation_already_started is returned, budget for the job to bill and fetch its result.

import os, requests

BASE = "https://api.sume.com"
H = {"Authorization": f"Bearer {os.environ['SUME_API_KEY']}"}

def cancel(job_id: str) -> str:
    r = requests.post(f"{BASE}/v1/jobs/{job_id}/cancel", headers=H, timeout=30)
    if r.ok:
        return "canceled"
    err = r.json().get("error", {})
    code = err.get("code")
    if code == "job_not_cancelable":
        return "terminal:" + err["details"]["status"]
    if code == "job_generation_already_started":
        return "running"
    r.raise_for_status()
    return "unknown"

print(cancel("job_123"))

What does Sume not do here?

Sume does not let you stop a job after generation has started, and the docs are explicit about it. If you need a ceiling on spend, reduce the request before you submit it or cap the work on the run surfaces that support a spend cap. Keep the request_id from any 409 for support.

Quick checklist

The points above reduce to a short list you can paste into a runbook.

  • Treat job_not_cancelable as final: retryable is false, so do not loop on it.
  • Cancel only jobs you expect to be queued; use cancel_url from the envelope to decide.
  • Keep the request_id from any 409 for support, and never log your API key.
  • After job_generation_already_started, keep polling to a terminal state and budget for the bill.
  • On shutdown, a second cancel pass over the same ids is safe because an already canceled job returns the same canceled job.

Sources

Related posts

More in Developers

All Developers posts

Written by Sume