The answer. Anything slower than a couple of seconds, or anything that talks to a flaky third party, should leave the request path. The shape that works: the HTTP handler stores the input (R2 for files, D1 for rows), inserts a status row with state = 'pending', sends the id to a Queue, and returns 202 Accepted with a Location pointing at /jobs/:id. The consumer does the slow part, writes the result and state = 'done' (or 'failed' with a reason) to the same row, and acks. The client polls the status URL, backing off, and stops on a terminal state. Nothing ever waits on the consumer, so a retry storm downstream cannot hold a browser connection open, and a 202 honestly means "accepted", which is all the producer can promise. Our guide-delivery Worker answers exactly this way: the form post is accepted once the row and the queue message exist, and delivery is proven later by the consumer's write.
The pattern.
// producer
const id = crypto.randomUUID();
await env.BUCKET.put(`uploads/${id}`, request.body, { httpMetadata: { contentType } });
await env.DB.prepare("insert into jobs (id, state) values (?, 'pending')").bind(id).run();
await env.JOBS.send({ id });
return new Response(null, { status: 202, headers: { location: `/jobs/${id}` } });
// consumer
const result = await analyze(await env.BUCKET.get(`uploads/${job.id}`));
await env.DB.prepare("update jobs set state = 'done', result = ? where id = ?").bind(JSON.stringify(result), job.id).run();
msg.ack();
GET /jobs/:id -> { "state": "pending" | "done" | "failed", "result"?: ..., "error"?: ... }
Watch out.
- Make the producer idempotent with a client-supplied key, or a double-click creates two jobs and two charges.
- The status row is the contract; write it before enqueueing, so a consumer can never find a message with no row.
- Cap polling on the client (max attempts, growing interval) and show
failedwith the reason; a spinner that never stops is a bug report.
Related: queues-batches-acks-and-retries · d1-migrations-prepared-statements-and-batch · r2-upload-through-worker-or-presign-for-big-files