Resume polling Sume video jobs after a restart with a sqlite ledger
Write the job id and Idempotency-Key to sqlite before anything else can crash, then resume polling open jobs on start. Python standard library only.

A video job outlives your process. If your worker restarts mid-wait, the job keeps running on Sume's side, and GET /v1/videos/{id} still answers. The only thing you can lose is the job id, so write it to disk first. The Python script below keeps a sqlite table of job ids with status and cost, and on start it polls every job that is not completed, failed or cancelled until all are closed.
Where a crash loses data
The risky gap is between sending the POST and storing the id. If the process dies there, Sume has a job you cannot name. The Idempotency-Key closes the gap: store the key before the POST, and after a restart send the same submit again; Sume returns the original job instead of creating a second one. The table sets out each crash point.
| Crash happens | What is lost | Recovery |
|---|---|---|
| Before the POST | Nothing | Submit again |
| After the POST, before the id is stored | The job id | Repeat the POST with the stored Idempotency-Key and get the original job |
| After the id is stored, during polling | Nothing | resume() polls the open ids |
| After completion, before download | The local file | Fetch the content route again |
The ledger
The jobs table has three columns: id, status and cost. The script inserts a row with status pending using insert or ignore, so running it twice is harmless. resume() selects every row whose status is not terminal, polls it, writes the new status and usage.cost, and sleeps between rounds. It returns when the open list is empty.
The sample inserts a placeholder id (job_1) to show the shape. In real code, insert the id you receive from the 202, and store the Idempotency-Key in its own column before you submit.
import json, os, sqlite3, time, urllib.request
BASE = "https://api.sume.com"
db = sqlite3.connect("jobs.db")
db.execute("create table if not exists jobs (id text primary key, status text, cost real)")
def get(path):
req = urllib.request.Request(BASE + path, headers={"Authorization": "Bearer " + os.environ["SUME_API_KEY"]})
with urllib.request.urlopen(req) as r:
return json.load(r)
def resume(delay=2.0):
while True:
open_ids = [r[0] for r in db.execute("select id from jobs where status not in ('completed','failed','cancelled')")]
if not open_ids:
return
for job_id in open_ids:
s = get(f"/v1/videos/{job_id}")
db.execute("update jobs set status=?, cost=? where id=?", (s["status"], (s.get("usage") or {}).get("cost"), job_id))
db.commit()
print(job_id, s["status"])
time.sleep(delay)
# at submit time, write the id before anything else can crash:
db.execute("insert or ignore into jobs (id, status) values (?, 'pending')", ("job_1",))
db.commit()
resume()Adjust before production
Add a deadline per job, and a counter for transient read errors so one 5xx does not end the loop. Use the video route and the status vocabulary of /v1/videos: pending, in_progress, completed, failed, cancelled. If you also read /v1/jobs/{id}/status, note that its vocabulary is queued, processing, completed, failed and canceled, with one l in canceled.
For many jobs, poll with a spread of intervals instead of one round every 2 seconds; the read budget is generous, but the logs are not.
A good test is to kill the process on purpose. Submit two cheap jobs, stop the script after the first row is written, start it again, and check that both rows reach a terminal status without a second submit. If a row stays pending after the job completed, the update statement or the commit is missing. Keep the database file out of version control. Also record the model and the requested duration in the ledger, so the cost column can later be checked against the price list for that clip.
- Commit after every row update.
- Keep the ledger file on durable storage.
- Compare ledger totals against usage in the dashboard.
Sources
Related posts
More in Developers
- Ruby Net::HTTP: Gemini Omni Flash 1.1 vertical 6 s clip, $0.75
Ruby standard library only: submit a 6-second 9:16 Gemini Omni Flash 1.1 clip to Sume ($0.75 at 720p) and poll to completion. Prices at four resolutions.
- Run receipt micros: 5,000,000 is $5.00 and 240,000 is $0.24
Sume run receipts report spend in USD micros. How to convert the cap and billable fields to dollars, and why GET /v1/usage stays the billing ledger.
- Scheduled run input: 64 properties and 2 MiB, how to pack a brief
A Sume scheduled run takes an input object of at most 64 properties and 2,097,152 bytes. Here is how to pack a brief for an agent without hitting either limit.
- Self-hosted H3 video server: API key, TLS and read-only media
MiniMax's H3 guide says to require an API key and TLS before public exposure and mount reference media read-only. A checklist, and what a hosted API gives you.
Written by Sume