Cancel queued Sume jobs on SIGTERM during a deploy
On shutdown, cancel jobs you no longer need before they start and leave started ones alone. A 21-line Node handler using POST /v1/jobs/{id}/cancel.

When a worker that submitted Sume jobs gets SIGTERM during a deploy, call POST /v1/jobs/{id}/cancel for the jobs it still tracks. Cancelling succeeds only before generation starts, so queued jobs are cancelled and their reservations released, while a job that has started answers 409 job_generation_already_started and runs to the end. Treat that 409 as a normal outcome and persist those ids so the next process can keep watching.
This matters most for a worker with a big fan-out on a small plan. Free allows 6 accepted jobs and Pro 24, per the generation admission page, and an abandoned queue of jobs nobody will read still occupies that capacity until it runs, and bills when it completes.
What cancel does and does not do
| Job state | Cancel result | Billing |
|---|---|---|
| queued | Canceled; the job becomes terminal | Reservation released or refunded where applicable |
| processing, generation started | 409 job_generation_already_started, details.cancelable false | Completes or fails normally |
| already canceled | Idempotent; returns the same canceled job | None |
| completed | Not cancelable | Already captured |
The handler
Keep a Set of ids this process submitted and has not seen finish; delete from it when a job reaches a terminal state. Promise.allSettled makes one failing cancel not hide the others, and the function returns a pair per id for logging.
const BASE = process.env.SUME_BASE_URL ?? "https://api.sume.com";
const headers = { Authorization: `Bearer ${process.env.SUME_API_KEY}` };
export const inFlight = new Set(); // job ids this process submitted and has not finished
export async function cancelPending(ids = [...inFlight]) {
const results = await Promise.allSettled(ids.map(async (id) => {
const res = await fetch(`${BASE}/v1/jobs/${id}/cancel`, { method: "POST", headers });
if (res.ok) return "canceled";
const body = await res.json().catch(() => ({}));
if (body.error?.code === "job_generation_already_started") return "running"; // will finish and bill
throw new Error(`${id}: ${res.status} ${body.error?.code ?? ""}`);
}));
return ids.map((id, i) => [id, results[i].status === "fulfilled" ? results[i].value : results[i].reason.message]);
}
export function cancelOnShutdown(exit = process.exit) {
process.once("SIGTERM", async () => {
for (const [id, outcome] of await cancelPending()) console.log(id, outcome);
exit(0); // running jobs keep going: store their ids so a restart can resume watching
});
}Wiring it in
The decision to cancel is a product decision. A rolling deploy that cancels every in-flight job turns each deploy into a customer-visible failure.
- Add the job id to
inFlightstraight after a successful submit, and persist it to your database too. - Remove it when status reads
completed,failedorcanceled. - Give the process a termination grace period longer than a few cancel calls. Container platforms usually kill after a fixed timeout.
- Do not cancel jobs a user is still waiting for just because your process is restarting; hand them over by id instead.
Limits
I tested it on Node 22 against a mock returning a cancelled job, the documented 409, and a 500: the output was canceled, running and an error line for the three ids. SIGKILL and crashes skip handlers, so this reduces waste but cannot replace recovery by id. Cancelling is a write, so it spends the write budget, 120 per minute on Free up to 1200 on Scale; with at most 120 accepted jobs on any self-serve plan, one sweep fits inside it.
Sources
Related posts
More in Developers
- cancel_url is null on a processing job: when Sume allows cancel
Why a Sume job in processing can show cancelable false and cancel_url null: cancel depends on whether generation work started, not the status label.
- Check a long avatar video for face drift: 24 inspect stills
After joining avatar jobs, sample the face across the whole video. Sume Video inspect returns up to 24 stills at times you pick, and the probe is unbilled.
- Check a TTS take with an STT round trip: flag skipped words in Python
Transcribe your own voiceover with Sume STT and diff it against the script to catch skipped or changed words before you ship. Python sketch included.
- Check a video request against /v1/videos/models before you submit
Duration, resolution, size and seed errors cost a round trip. A short Python validator reads the model catalog and refuses a bad request locally first.
Written by Sume