A queue carries messages from the code that sends them (a producer) to the code that handles them (a consumer). Use it to move slow work, such as sending email, calling APIs, or resizing uploads, out of the request. Queues run on Cloudflare Queues. There is no worker process to run: Cloudflare delivers messages to your app in batches.
On a Laravel container app, the queue is also a Laravel queue connection, so dispatch() works unchanged.
Tip
A queue resource pushes batches into your app. If you want long-running queue:work processes on Redis or a database queue instead, with autoscaling and failed-job tools, use Queue workers.
Create a queue
- Open your app. On Overview, choose Add resource, then Queue.
- Keep Create new selected and enter a Name, such as
jobs. - Choose Create, then redeploy the app.
To use a queue your organization already has, choose Attach existing, pick it under Existing Queue, and choose Attach.
On an SSR or hybrid app, the Jobs section can also create and attach queue bindings.
The Queues page
Your organization's queues are also listed under Queues in the top navigation (/projects/queues). There you can:
- Create a queue: enter a name (lowercase letters, numbers, and dashes) and choose Create queue.
- See each queue's Waiting messages and which apps it is Bound to.
- Send test to put a
{"dply_test": true}message on it. - Attach to a project: pick a Queue…, a Project…, and a binding name in
UPPER_SNAKE_CASE(defaultJOBS), then choose Attach and redeploy that app. - Delete a queue.
Producers and consumers
Every app a queue is attached to can send to it. Only one app runs its jobs: a Cloudflare queue has one consumer.
- The earliest-created production app attached to the queue is the consumer. Every other app only sends. Its card says "Sends only. {app} runs these jobs."
- Previews can send to a queue but never consume it, so they never take production's messages.
- A sleeping queue connection does not make an app the consumer.
- If no production app consumes the queue, messages wait.
The queue's sheet shows this under Who runs the jobs and on the Consumers tab. Each app's Jobs section shows it too: Runs here, Sends to {app}, or Nothing runs it.
Delivery settings
dply sets the consumer's delivery settings. Batch size and retries are the same on every plan. Batch wait and concurrency come from your plan, in config/product/subscription.php:
| Plan | Batch size | Retries | Batch wait | Concurrent batches |
|---|---|---|---|---|
| Starter | 10 | 5 | 2 s | 5 |
| Pro | 10 | 5 | 2 s | 10 |
| Team | 10 | 5 | 1 s | 50 |
| Enterprise | 10 | 5 | 0 s | Automatic (Cloudflare's maximum) |
Batch wait is the longest a partial batch waits before it is delivered. A container app picks up a plan change on its next deploy.
dply does not set a dead-letter queue. A message that fails all 5 retries is dropped.
Limits
Your plan sets how many queues your organization can have. Queues created from any app and from the Queues page count together.
| Starter | Pro | Team | |
|---|---|---|---|
| Sites | Unlimited | Unlimited | Unlimited |
| Concurrent builds | 1 | 2 | 5 |
| Build timeout | 20 min | 45 min | 60 min |
| Custom domains (per organization) | 3 | 20 | 100 |
| Container app instances | 1 per app | Autoscaling | Autoscaling |
| Queue workers per app | 1, starts when jobs arrive | 5, autoscaling | 10, autoscaling |
| SQL databases (D1) | 2 | 10 | 50 |
| Queues | 2 | 10 | 50 |
| Realtime connections per app | 200 | 1,000 | 5,000 |
| Audit log | No | No | Yes |
A message can be up to 128 KB. Laravel jobs can be delayed up to 12 hours.
Laravel (container apps)
When a queue is attached and awake, the next deploy sets:
QUEUE_CONNECTION=dply
DPLY_QUEUE=JOBS
DPLY_QUEUE is the binding name of the app's first attached queue. Values you save under Environment win. Then dispatch as usual:
dispatch(new ProcessOrder($order));
// A second attached queue, by its binding name:
dispatch(new ProcessOrder($order))->onQueue('REPORTS');
The app's Worker receives each batch and POSTs it to /_dply/queue in your app, where dply/laravel runs the jobs. A job that throws is retried. There is no queue:work to run.
Important
dply/laravel provides the dply connection. dply adds it on the next deploy when the app does not require it, but only when dply builds the image. If your repository has its own Dockerfile, run composer require dply/laravel.
Rails apps use gem "dply-rails" and config.active_job.queue_adapter = :dply.
Any language (container apps)
Send a message by POSTing its body to the queue's private host. Copy the host from the queue's Connect tab.
await fetch('http://dply.my-app.jobs.internal/send', {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify({ order: 42 }),
});
Consume by handling POST /_dply/queue in your app. The app's Worker sends each batch there:
{
"queue": "JOBS",
"messages": [
{ "id": "…", "body": "{\"order\":42}", "attempts": 1 }
]
}
The request carries an x-dply-queue-token header equal to the DPLY_QUEUE_TOKEN environment variable; reject requests without it. Answer 200 with the ids that failed. Those are retried after 30 seconds, and the rest are acknowledged. Any non-2xx answer retries the whole batch after 30 seconds.
import express from 'express';
const app = express();
app.use(express.json());
app.post('/_dply/queue', async (req, res) => {
if (req.get('x-dply-queue-token') !== process.env.DPLY_QUEUE_TOKEN) {
return res.status(403).end();
}
const failed = [];
for (const message of req.body.messages) {
try {
await handle(JSON.parse(message.body));
} catch {
failed.push(message.id);
}
}
res.json({ failed });
});
Worker apps (SSR and hybrid)
The queue is a producer binding at env.NAME. Send any JSON value. To consume, export a queue handler from your Worker. dply's platform Worker delivers each batch to the app that owns the queue.
export default {
async fetch(request, env) {
await env.JOBS.send({ order: 42 });
return new Response('queued', { status: 202 });
},
async queue(batch, env) {
for (const message of batch.messages) {
try {
await handle(message.body);
message.ack();
} catch {
message.retry({ delaySeconds: 30 });
}
}
},
};
A message you neither acknowledge nor retry is acknowledged. If the handler throws, the whole batch is retried.
Watch and test a queue
Choose Open on the queue's card. The sheet has these tabs:
- Overview: Waiting messages (last hour), Operations and Cost this month, who runs the jobs, and the Delivery settings.
- Connect: code samples for this queue.
- Send a test: enter a Message (JSON) and choose Send message. The consumer receives exactly this body, so send something it expects or ignores.
- Consumers: the queue's consumer script and settings.
Pricing
Queues bill every operation from the first:
| Meter | Price | Unit |
|---|---|---|
| Operations | $0.52 | per million |
Every write, read, and delete of a message is an operation, so a delivered message costs about three. Usage is collected daily. This bills at the invoice line SQL, queues and key-value.
Delete a queue
From the app, choose Delete on the card, or Delete queue in the sheet. This removes the queue and every message waiting in it.
Cloudflare refuses to delete a queue that a live deploy still binds. So when the app has a live deploy, dply detaches the queue now and deletes it after your next deploy: "Detached. The live app still uses it, so it is deleted after the next deploy — redeploy to finish." If another app in your organization still uses the queue at that point, dply keeps it. See Detach or delete.
Warning
The Delete button on the Queues page does not wait for a deploy and does not detach the queue from apps. If a deploy still binds it, Cloudflare refuses and the page shows the error. Otherwise the queue is deleted right away but stays attached to its apps, and their next deploy binds a queue that no longer exists. Delete a queue from the app instead.