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.

4 min readSume
All posts

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.

Job statuses from the Sume jobs docs, read 2026-10-05
StatusTerminalRank
queuedNo0
processingNo1
completedYes2
failedYes2
canceledYes2

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

All Developers posts

Written by Sume