User closed the tab mid-render: cancel the Sume job or let it finish?

Cancel works only before generation starts; after that you get 409 job_generation_already_started and the job bills. A tested tab-close handler.

5 min readSume
All posts

When a user closes the tab while a video renders, send one cancel request and treat the answer as information, not as a failure: it succeeds only before generation work starts, and after that Sume returns 409 job_generation_already_started with details.cancelable: false and the job runs to completion. Sume jobs and results says the same, and adds that cancelling a job that is already canceled is idempotent. So the right design is cancel-if-possible, then keep the job id so the finished clip is waiting when the user returns.

Whatever you choose, write it down as a product decision, because support will be asked about it. Do not assume that dropping the connection stops anything. A browser disconnect, a closed SSE stream or a client timeout ends your wait and nothing else.

Which policy should the tab-close handler use?

There are two honest policies. The first is to cancel on leave, which saves money when the job is still queued and does nothing harmful when it is not. The second is to never cancel and let every job finish, which suits a product where the result is saved to a library anyway. A mixed policy is common: cancel when the user explicitly presses Stop, keep the job when they merely navigate away. The code below implements the decision as a pure function so you can test it without a network. The cancel argument is any function that returns a status and a body, which lets the unit test fake every answer the API can give, including a transient 503 where the right move is to try again shortly rather than to guess.

What does the handler do with each answer?

def on_leave(cancel, job_id, user_pressed_stop):
    """cancel(job_id) -> (http_status, body). Returns the action taken."""
    if not user_pressed_stop:
        return "keep"
    status, body = cancel(job_id)
    if status == 200:
        return "canceled"
    err = (body or {}).get("error", {})
    if status == 409 and err.get("code") == "job_generation_already_started":
        return "running-keep-and-save"
    if status == 409 and err.get("code") == "job_not_cancelable":
        return "terminal-fetch-result"
    return "retry-later"

def fake(status, code=None):
    body = {"error": {"code": code}} if code else {}
    return lambda job_id: (status, body)

assert on_leave(fake(200), "j1", True) == "canceled"
assert on_leave(fake(200), "j1", False) == "keep"
started = fake(409, "job_generation_already_started")
assert on_leave(started, "j1", True) == "running-keep-and-save"
assert on_leave(fake(503), "j1", True) == "retry-later"
print("tab-close policy ok")

Why does the 409 branch keep the job?

The running-keep-and-save branch matters most. It means: stop showing a spinner, record that the job id belongs to this user, and let a webhook or a later poll attach the artifact to their library. Sume bills the job, so throwing the id away turns a wanted clip into a pure loss. The terminal-fetch-result branch is the opposite case, where the job already finished and the only useful step is a GET result_url.

Keep the cancel call itself safe to repeat. A repeated cancel of a canceled job is idempotent, so a flaky network that retries it does no harm, and you can reuse the same retry wrapper as your other idempotent calls.

Cancel outcomes and the next step, from Sume jobs and results (read 2026-10-06)
AnswerMeaningYour next step
200 canceledStopped before generationFree the user's slot
409 job_generation_already_startedGeneration began, runs to completionSave the id and fetch later
409 job_not_cancelableAlready terminalFetch the result or the error
Repeat cancel on a canceled jobIdempotentTreat as success

What do I tell the user?

One boundary to respect: a client timeout is not a cancel and a cancel is not a refund promise. The billing rules for failed and canceled work live in Sume errors and credits, and you should read them before you describe the outcome to a user. In your own interface, say what happened in plain words, such as This render had already started, so it will finish and appear in your library, rather than showing a raw error code. Log the job id and the branch taken, so a later complaint of the form I closed the page and was still charged can be answered from a record: the job had already started when the cancel arrived, and the clip is in the library. If you offer a Stop button, put the same message next to it before the click, so people learn that stopping late does not recover the spend.

Sources

Related posts

More in Developers

All Developers posts

Written by Sume