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 retried 10 times and stopped: recover the job anyway
Sume tries a webhook up to 10 times, 30 seconds apart, 10s timeout each. After that the job is still terminal. Poll status_url or call redeliver to recover.
- Sume webhook Redeliver vs Send test: which one replays a real job?
Send test posts a dummy webhook.test payload to a URL you type. Redeliver re-posts a real job's terminal event with a fresh signature. API routes for both.
- 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.
Written by Sume