Skip to content
RT
Go back

Cloudflare Queues: respond fast, process later

One of the most useful backend rules:

If the user doesn’t need the result right now, don’t block the request waiting for that work.

Typical examples:

Cloudflare Queues help you split two paths:

user experience  ->  fast request
internal work    ->  messages in a queue

If you came from Workers: the Worker stays the entrypoint; the Queue takes work that shouldn’t live inside fetch.

Terms (keep them clear)

TermMeaning
Queuemessage queue
Messageunit of work
Producercode that sends messages
Consumercode that reads and processes
Retrycontrolled reattempt
DLQDead Letter Queue — messages that exhausted retries

Base flow:

request -> producer -> Queue -> consumer

Mental model: checkout

When someone buys, your system may need to:

  1. confirm the request
  2. respond to the user
  3. send a receipt
  4. update CRM
  5. record analytics
  6. notify support

Not all of that has to finish before you respond.

You can return:

{ "accepted": true }

and leave jobs on a Queue for later.

Producer and consumer in Workers

Producer from fetch():

type ReceiptJob = {
  orderId: string;
  email: string;
};

export interface Env {
  RECEIPT_QUEUE: Queue<ReceiptJob>;
}

export default {
  async fetch(request, env): Promise<Response> {
    const job = await request.json<ReceiptJob>();

    await env.RECEIPT_QUEUE.send({
      orderId: job.orderId,
      email: job.email,
    });

    return Response.json({ accepted: true }, { status: 202 });
  },
} satisfies ExportedHandler<Env>;

Consumer:

export default {
  async queue(batch, env, ctx): Promise<void> {
    for (const message of batch.messages) {
      try {
        await sendReceipt(message.body);
        message.ack();
      } catch (error) {
        console.error("receipt_failed", {
          orderId: message.body.orderId,
          error,
        });
        message.retry({ delaySeconds: 30 });
      }
    }
  },
} satisfies ExportedHandler<Env, ReceiptJob>;

Concept:

fetch produces
queue consumes

RECEIPT_QUEUE exists as a binding. The message type is part of the design, not a cosmetic detail.

Why 202 Accepted often fits

202 means:

I received the request and accepted it for processing; the full work may still be pending.

You don’t always need it. For async flows it’s more honest than faking a 200 OK when the receipt, CRM, and webhook haven’t run yet.

Retries: errors don’t vanish

A Queue doesn’t erase failures. It gets them off the main path and gives you a way to retry.

External systems fail:

If all of that lives inside the request, the user pays the cost. With a Queue you can respond fast and process with more control.

DLQ: not a trash can—evidence

DLQ = Dead Letter Queue: messages that hit the retry limit.

Don’t think “where bad things go.”

Think:

investigation tray for messages that couldn’t be processed.

A DLQ helps you answer:

Without a DLQ, a lot of async failure stays invisible or gets lost.

Idempotency: assume redelivery

In async systems, assume a message may be processed more than once.

Idempotent:

processing the same message twice must not double a dangerous side effect.

Bad:

send receipt

Better:

send receipt for orderId if receipt_sent = false

Or:

add CRM event with idempotency key = jobId

“Use a Queue” isn’t enough. You have to design the consumer.

When I would use Queues

Good signals:

Common patterns:

checkout -> Queue -> receipt email
upload   -> Queue -> image resize
webhook  -> Queue -> normalization
form     -> Queue -> CRM sync
analytics-> Queue -> batch write

When I wouldn’t put everything on a Queue

It may be the wrong pattern if:

ProblemBetter tool
Durable multi-stepWorkflows
Per-entity stateDurable Objects
Simple requesta Worker is enough

Production checklist

  1. Define the message type.
  2. Include jobId, orderId, or another stable key.
  3. Decide which errors retry.
  4. Decide which errors go to the DLQ.
  5. Make the consumer idempotent.
  6. Log with context.
  7. Measure backlog, retries, and failures.
  8. Document who produces and who consumes.

A Queue without observability is a black box. With logs and a DLQ it becomes an operational tool.

The line to remember

Worker fetch = producer
Queue = message buffer
queue handler = consumer
retry = controlled reattempt
DLQ = investigation (and possible reprocess)

Respond fast. Process later. Always observe.


Cloudflare series for builders: Workers · Bindings · Durable Objects · Queues

Docs: Queues · How Queues works · Getting started · Batching, retries, delays · Dead Letter Queues


Share this post on:

Next Post
How I start indie projects: Better-T-Stack and the tools inside it