Skip to content
4th April 2025: This is a preview, whilst production-ready, it means some APIs might change

Queues

Whilst building a webapp you want to be able to respond as quickly as possible to user interactions, but sometimes you need to do things that take a long time! For example, you might need to send an email when a user submits a form, or you need to process a payment, or do something magic with AI - and you don’t want the user to wait for these things to complete.

To handle these you’ll use background tasks. Background tasks are managed by the Cloudflare queue system. You send a message to a queue, where a worker will process the message, but on a different worker - so it doesn’t block the main one.

Setup

First thing you’ve got to do is create a queue and bind the queue producers and consumers to your worker.

Terminal window
npx wrangler queues create my-queue-name

Replace my-queue-name with the name of your queue, and place the following in your wrangler.jsonc file:

wrangler.jsonc
{
"queues": {
"producers": [
{
"binding": "QUEUE",
"queue": "my-queue-name",
}
],
"consumers": [
{
"queue": "my-queue-name",
"max_batch_size": 10,
"max_batch_timeout": 5
}
]
}
}

This will bind the queue to the env.QUEUE object in the worker. So you’ll be able to send messages.

Naming Queues

Queue names must match the following RegEx pattern: ^[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?$

Valid queue names
  • my-queue
  • my-awesome-queue-123
  • queue1
  • 1queue
  • my-queue-v2
Invalid queue names
  • My_Queue (uppercase letters not allowed)
  • MY_QUENE_NAME (uppercase letters and underscores not allowed))
  • -queue- (cannot start or end with hyphen)
  • really-really-really-really-really-really-really-long-queue-name (max 63 chars)
  • queue_name (underscores not allowed)

Sending messages

src/worker.tsx
import { env } from "cloudflare:workers";
export default defineApp([
route('/pay-with-ai', () => {
// Post a message to the queue
env.QUEUE.send({
userId: 1,
amount: 100,
currency: 'USD',
})
return new Response('Done!')
})
])

Receiving messages

In order to “consume messages” from the queue you need to change the shape of the default export of your worker. You’ll add another function called queue that will receive a batch of messages.

src/worker.tsx
const app = defineApp([ /* routes... */])
export default {
fetch: app.fetch,
async queue(batch) {
for (const message of batch.messages) {
console.log('handling message' + JSON.stringify(message))
}
}
} satisfies ExportedHandler<Env>;

This will receive a batch of messages, and process them one by one.

Ways to Send Messages

Cloudflare Queues allow Workers to send and process asynchronous messages reliably.

There are three common approaches to send data:

Send Message Body Directly (up to 128KB)

Best for: Small payloads that fit within the 128KB limit.

await queue.send({
body: JSON.stringify({ email: "[email protected]", subject: "Welcome!" }),
});

✅ Simple and fast ❌ Hard limit of 128KB per message

Store in R2 and Send Object Key

Best for: Large payloads (e.g., files, JSON blobs, videos).

// Upload to R2 first
await r2.put("msg/123.json", JSON.stringify(largeData));
// Then send only the key to the queue
await queue.send({
body: JSON.stringify({ r2Key: "msg/123.json" }),
});

✅ Great for large data ✅ Persistent and versioned if needed ❌ Slightly more complex (requires R2 integration)

Store in KV and Send KV Key

Best for: Short-lived messages or small-to-medium payloads.

// Save to KV
await kv.put("queue:msg:123", JSON.stringify(data), { expirationTtl: 600 });
// Send reference key
await queue.send({
body: JSON.stringify({ kvKey: "queue:msg:123" }),
});

✅ Fast access ✅ Automatic expiration possible ❌ Not ideal for large data ❌ KV has eventual consistency (meaning that when you write data to Cloudflare KV (via kv.put), it might not be immediately visible to all readers — especially in different Cloudflare data centers.)

Tips

Handling Different Queues

By sending a message on a queue

src/worker.tsx
import { env } from "cloudflare:workers";
export default defineApp([
route('/pay-with-ai', () => {
// Post a message to the queue
env.QUEUE.send({
userId: 1,
amount: 100,
currency: 'USD',
})
return new Response('Done!')
})
])

when handling queues and receivng a MessageBatch, the batch contains a collection of messages and the name of the queue which you can use to handle

src/worker.tsx
const app = defineApp([ /* routes... */])
export default {
fetch: app.fetch,
async queue(batch) {
if (batch.queue === 'my-queue-name') {
for (const message of batch.messages) {
console.log('handling my-queue-name message' + JSON.stringify(message))
}
}
}
} satisfies ExportedHandler<Env>;

ℹ️ Note: Having a dedicated Queue for a specific message is a best practice

Handling Different Messages on the Same Queue

Use metadata (e.g. type, source, key) in your message body to help the consumer Worker determine where and how to retrieve the full data.

If for some reason, you decide to share a queue for different types of messages, one pattern is to set a type (or other attribute) in the message body to specify its purpose and how to handle its contents.

For example, when sending this payment message:

src/worker.tsx
import { env } from "cloudflare:workers";
export default defineApp([
route('/pay-with-ai', () => {
// Post a message to the queue
env.QUEUE.send({
type: 'PAYMENT',
userId: 1,
amount: 100,
currency: 'USD',
})
return new Response('Done!')
})
])

One can then determine the message type and handle accordingly:

src/worker.tsx
const app = defineApp([ /* routes... */])
export default {
fetch: app.fetch,
async queue(batch) {
for (const message of batch.messages) {
const { type, userId, amount, currenct } = message.body as { type: string, userId: number, amount: number, currency: string };
if (type === 'PAYMENT') {
console.log('handling payment message' + JSON.stringify(message))
}
}
}
} satisfies ExportedHandler<Env>;