Sume webhook handler over 10 seconds: acknowledge first, then work
Sume gives each webhook attempt 10 seconds. Verify, answer 2xx, then process in the background and dedupe on job_id. Node sample you can run locally.

Sume delivers each job webhook with a 10 second timeout per attempt, and retries up to 10 times with fixed 30 second spacing. If your handler copies the image to storage, writes rows and publishes a page before it answers, a slow day turns into timeouts, retries and duplicate work. The webhook docs tell you to treat job_id as the idempotency key.
The shape that avoids this is short: verify the signature on the raw body, answer with a 2xx, and do the slow part afterwards.
What a slow handler costs
| Handler outcome | What Sume does | Your risk |
|---|---|---|
| 2xx inside 10 s | Delivery done | None |
| Slower than 10 s or non-2xx | Retries, up to 10 attempts, 30 s apart | Same event arrives again while work is half done |
| All attempts used | Stops automatic retries | Use POST /v1/jobs/{id}/webhook/redeliver (jobs:write) |
Node sample
It refuses to start with an empty secret, checks the 300 second timestamp window, accepts any sume-v1= entry in the header (rotation sends two), and answers 202 before the slow work runs.
import http from "node:http";
import { createHmac, timingSafeEqual } from "node:crypto";
const secret = process.env.SUME_COM_WEBHOOK_SIGNING_SECRET ?? "";
if (!secret) throw new Error("SUME_COM_WEBHOOK_SIGNING_SECRET is empty");
const seen = new Set();
function valid(raw, h) {
const ts = h["x-sume-webhook-timestamp"];
if (!ts || Math.abs(Date.now() / 1000 - Number(ts)) > 300) return false;
const want = createHmac("sha256", secret).update(`${ts}.${raw}`).digest("hex");
return String(h["x-sume-webhook-signature"] ?? "").split(",").some((p) => {
const got = p.replace("sume-v1=", "");
return got.length === want.length && timingSafeEqual(Buffer.from(got), Buffer.from(want));
});
}
async function work(event) { // the slow part: copy media, write rows, publish
if (seen.has(event.job_id)) return; // dedupe on job_id
seen.add(event.job_id);
console.log("processing", event.job_id, event.event);
}
http.createServer((req, res) => {
let raw = "";
req.on("data", (c) => (raw += c));
req.on("end", () => {
if (!valid(raw, req.headers)) return res.writeHead(401).end();
res.writeHead(202).end(); // acknowledge inside the 10 s window
setImmediate(() => work(JSON.parse(raw)).catch(console.error));
});
}).listen(8787);Make the background step safe
- The in-memory
Setis only for the demo. Use a unique constraint onjob_idin your database so a retry or redelivery cannot insert twice. - On a serverless host that freezes the process after the response, enqueue the event to a durable queue instead of using
setImmediate. - Keep a poll as backup: a webhook that never arrived is invisible, and
GET /v1/jobs/{id}/statusis the cheap way to reconcile.
Sources
Related posts
More in Developers
- What status should a Sume webhook receiver return? Any 2xx
Sume counts any 2xx as delivered, so 202 or 204 is fine. A tested receiver that verifies the signature, stores the job id, answers 204 and defers the work.
- Sume webhook Redeliver errors: 409 running or no URL, 403 jobs:write
POST /v1/jobs/{id}/webhook/redeliver returns 409 while the job runs or if it had no webhook_url, 403 without jobs:write, and 404 for a job you cannot see.
- Sume webhook Redeliver gets a fresh signature: dedupe on job_id
Redeliver re-POSTs a job's real terminal event with a new timestamp and signature, so dedupe on job_id and event, never on the signature or timestamp.
- Sume webhook retries last 270 to 370 seconds: when to poll
Sume tries a job webhook up to 10 times, 30 seconds apart, with a 10 second timeout each. That is about 4.5 to 6 minutes of retries before you must poll.
Written by Sume