Skip to content
Built with

Queues ​

Build & Deploy

Use queues for work that can happen later, such as sending emails or processing uploads. Define a consumer in queues/, then send it typed messages from your app.

On this pageDefine a queue consumer · Send typed messages · Acknowledge or retry messages

Defining queues ​

This step creates a consumer in queues/. .ts, .mts, .js and .mjs files work. The path sets the queue name: queues/emails.ts creates emails, and queues/order/notifications.ts creates order-notifications. Each queue must have a unique name.

The name must follow Cloudflare's rules: 1–63 characters of letters, digits and -, starting and ending with a letter or digit.

Export a default handler wrapped in defineQueue<T>. T defines the message body for both the consumer and calls to send():

ts
// queues/emails.ts
import { defineQueue } from 'void';

interface Message {
  to: string;
  subject: string;
}

export default defineQueue<Message>(async (batch, env) => {
  for (const msg of batch.messages) {
    console.log(`Send to ${msg.body.to}: ${msg.body.subject}`);
    msg.ack(); // Acknowledge successful processing
  }
});

Files or directories starting with _ are ignored.

Sending messages ​

This step sends messages with the typed queues proxy from void/queues. Each queue is a property named after it, typed by the consumer's defineQueue<T>.

ts
// routes/api/orders.ts
import { defineHandler } from 'void';
import { queues } from 'void/queues';

export const POST = defineHandler(async (c) => {
  const order = await c.req.json();
  // Fully typed: body must match the
  // Message interface from queues/emails.ts
  await queues.emails.send({
    to: order.email,
    subject: 'Order confirmed',
  });
  return c.json({ ok: true });
});

Per-message acknowledgment ​

Each message has ack() and retry() methods, as in the Cloudflare Queues API:

ts
export default defineQueue<Message>(async (batch, env) => {
  for (const msg of batch.messages) {
    try {
      await processMessage(msg.body);
      msg.ack(); // Remove from queue
    } catch {
      msg.retry({ delaySeconds: 30 }); // Re-deliver after 30s
    }
  }
});
  • msg.ack(): acknowledge the message and remove it from the queue
  • msg.retry(options?): retry the message, optionally with a { delaySeconds } delay
  • batch.ackAll(): acknowledge all messages in the batch
  • batch.retryAll(options?): retry all messages in the batch

The first call wins for each message. Calling ack() after retry(), or the reverse, does nothing. Messages without a decision are acked if the handler succeeds and retried if it throws.

The timestamp field on each message is a Date object.

Consumer options ​

Show consumer options

This step sets batch delivery options with named exports.

ts
// queues/emails.ts
import { defineQueue } from 'void';

interface Message {
  to: string;
  subject: string;
}

export const maxBatchSize = 50;
export const maxBatchTimeout = 10; // seconds
export const maxRetries = 5;
export const retryDelay = 30; // seconds

export default defineQueue<Message>(async (batch, env) => {
  for (const msg of batch.messages) {
    console.log(`Send to ${msg.body.to}: ${msg.body.subject}`);
  }
});
  • maxBatchSize: maximum number of messages per batch (default 10)
  • maxBatchTimeout: maximum seconds to wait before delivering an incomplete batch (default 5)
  • maxRetries: maximum number of retries before delivery is exhausted (default 3)
  • retryDelay: seconds to wait between retries (default 0)

SDK-managed queues have no dead-letter queue. Without one, Cloudflare deletes messages that run out of retries. See Cloudflare's dead-letter queue guide.

The SDK provisions queues and connects their producers and consumers when you deploy.

Local development ​

In native SDK apps, a sent message runs the consumer in Miniflare automatically. Local delivery uses the same handler as production.

This step sends a batch by hand to the SDK's dev endpoint.

POST /__void/queue
Content-Type: application/json
{ "queue": "<queue-name>", "messages": [{ "id": "1", "timestamp": <unix_ms>, "body": <any>, "attempts": 1 }] }

The endpoint needs a local dev trigger token. At startup, the dev server prints a ready-to-paste curl command with it.

bash
curl -X POST http://localhost:5173/__void/queue \
  -H "Content-Type: application/json" \
  -H "x-void-dev-trigger: <printed-token>" \
  -d '{"queue":"my-queue","messages":[{"id":"1","timestamp":'"$(date +%s000)"',"body":{"hello":"world"},"attempts":1}]}'

Example output: The local queue consumer runs, and its logs appear in the dev server.

In supported meta-frameworks, use /__void/queue to test a consumer manually. It runs inside the framework adapter's development runtime with that adapter's bindings. In this mode, sending to a queue does not run the consumer.

Next steps ​

implemented.stream AI