Cloudflare Queue consumer 15 min wall time: poll a Sume job
A Queue consumer on Cloudflare can run 15 minutes of wall time, but a Sume job can wait longer. Re-enqueue with a delay and honor next_poll_after_seconds.

Do not hold a Cloudflare Queue consumer open until a Sume job finishes. The consumer has a 15 minute wall time, the job can run longer, and a Worker pays for the wait. Read the job once, return, and put the job id back on the queue to run after the number of seconds that next_poll_after_seconds gives.
The Cloudflare limits
The Workers limits page (updated 2026-09-05) lists the values below. ctx.waitUntil() extends work for up to 30 seconds after the response, which is enough to submit a job but not to wait for one.
| Limit | Value |
|---|---|
| Queue consumer wall time | 15 minutes |
ctx.waitUntil() after response | Up to 30 seconds |
| CPU time, Free | 10 ms |
| CPU time, Paid | 30 s default, up to 5 min |
| Subrequests per invocation | 50 Free, 10,000 Paid |
The re-enqueue loop
Each message holds a job_id. The consumer reads GET /v1/jobs/{id}, and if terminal is false it sends the message again with a delay. A message that waits in the queue costs nothing, and each read is one subrequest.
The Sume key goes in x-api-key. Do not send an Authorization header with it; both together return 401.
export default {
async queue(batch: MessageBatch<{ jobId: string }>, env: { SUME_API_KEY: string }) {
for (const msg of batch.messages) {
const res = await fetch(`https://api.sume.com/v1/jobs/${msg.body.jobId}`, {
headers: { "x-api-key": env.SUME_API_KEY },
});
if (res.status === 429) {
msg.retry({ delaySeconds: Number(res.headers.get("retry-after") ?? 30) });
continue;
}
const body: any = await res.json();
const job = body.data ?? body;
if (job.terminal) msg.ack();
else msg.retry({ delaySeconds: Math.max(2, job.next_poll_after_seconds ?? 5) });
}
},
};Prefer the webhook when you can
A webhook removes the loop. Sume signs each delivery and verifies with WebCrypto, which runs on Workers. Keep the queue poll as the fallback for a delivery that never arrives, since Sume gives up after 10 attempts.
retry-afteron a 429 is the wait to honor.- A canceled or failed job is terminal too; read
statusanderror. - Fetch
/resultonly whenresult_readyis true.
Why re-enqueue beats a long wait
A consumer that waits for a render holds a Worker invocation. The wall time is a ceiling, not a target, and the invocation has CPU and subrequest budgets as well. A message that is retried with a delay holds nothing while it waits, so the same fleet handles many more jobs. Each retry is one subrequest and a few milliseconds of CPU.
The poll interval comes from Sume. next_poll_after_seconds rises when the queue is long, which spreads reads out without you tuning anything. If you get 429 rate_limited, the retry-after header gives the exact wait.
Poison messages and limits
Put a maximum age on a job id in the message, for example 90 minutes for a video, and stop with an alert after that. A Sume job that is queued for a very long time usually means the workspace is at its plan concurrency, which is 1 on Free, 4 on Pro, 8 on Startup and 20 on Scale. Raise the plan or reduce submissions; polling harder does not help.
Sources
Related posts
More in Developers
- Convert MAI-Transcribe-2 word offsets to Sume video caption words
Turn word timings in milliseconds into the seconds-based words array Sume video-captions accepts. A short Python converter plus the 60 s and 1,200-word limits.
- Dart: submit a 30-second Wan 3.0 clip with package:http
Server-side Dart with package:http: POST wan-3.0 for 30 seconds, poll until completed and write the MP4. Keep the key out of the Flutter app.
- Deno 2.9.7 traceparent fix: tag a Sume job id in a Deno.serve log
Deno 2.9.7 extracts traceparent from Deno.serve regardless of header case. In a Sume webhook handler, log the job_id with the trace so a render is traceable.
- Deno: a 30-second Wan 3.0 video with fetch and top-level await
A short Deno script that submits a 30-second wan-3.0 job, polls to completion and writes clip.mp4, using only fetch and no npm packages.
Written by Sume