Sume webhook and status poll race: never move a job row backwards
A late poll can say processing after the webhook already said completed. A rank-guarded SQLite update keeps a Sume job row from going backwards.

The race
Sume recommends webhook mode with polling kept as a backup, because a webhook is a delivery optimization, not the only recovery path. Run both and you get two writers for one job row. A poll that started before the terminal event can return after it, and a naive UPDATE then writes processing over completed.
Terminal webhooks are the only job events Sume pushes: job.completed, job.failed and job.canceled. Everything before that you learn by polling, so the stale writer is almost always the poller.
Rank the statuses
Give each status a rank and only accept a write that raises it. Terminal statuses share the top rank, so the first terminal write wins and a second one, even a different terminal, changes nothing.
| Status | Terminal | Rank |
|---|---|---|
| queued | No | 0 |
| processing | No | 1 |
| completed | Yes | 2 |
| failed | Yes | 2 |
| canceled | Yes | 2 |
A guarded update
The insert-or-ignore creates the row if neither writer has seen the job yet, and the guarded update only moves it forward. Run this file as is; it prints 1 for the move that took effect and 0 for the stale one.
import sqlite3
RANK = {"queued": 0, "processing": 1, "completed": 2, "failed": 2, "canceled": 2}
db = sqlite3.connect(":memory:")
db.execute("create table jobs(id text primary key, status text, rank int)")
def advance(job_id, status):
r = RANK[status]
db.execute("insert or ignore into jobs values(?,?,?)", (job_id, status, r))
cur = db.execute(
"update jobs set status=?, rank=? where id=? and rank<?", (status, r, job_id, r))
return cur.rowcount
print(advance("job_1", "processing"))
print(advance("job_1", "completed"))
print(advance("job_1", "processing"))Rules that keep it honest
- Use job_id from the webhook as the idempotency key on your side; Sume retries non-2xx responses until it uses all attempts.
- Verify the signature on the raw body before you touch the table.
- Never fetch the result from a poll that reported processing; wait for terminal true or result_ready.
- If a webhook never arrives, the next poll closes the gap, because the job reached its real state either way.
Sources
Related posts
More in Developers
- Sume webhook five-minute replay window: reject stale deliveries
Sume signs timestamp.raw_body and advises a five-minute replay tolerance. Reject stale timestamps, refuse an empty secret, and keep job_id as the dedupe key.
- Sume webhook retries: 10 attempts 30 seconds apart, size your downtime
Sume retries a failing webhook up to 10 attempts at a default 30-second spacing, about 4.5 minutes. Past that, redeliver or poll after a publisher outage.
- Swift: submit a Kling 3.0 job and save the MP4 to disk
One Swift file: POST /v1/videos for kling-3, poll the polling_url, follow the content redirect and write out.mp4. A 5 s clip without audio costs $0.70.
- Swift URLSession: call Sume's image API and branch on 200 or 202
Use URLSession.data(for:) with async/await, cast the response to HTTPURLResponse, and read statusCode before you touch data[0].url on Sume's /v1/images.
Written by Sume