f you build on Cloudflare Workers today and you need background jobs, you have probably had the following conversation:
"We can use Cloudflare Queues for delivery, it's basically free and global."
"Yeah but then how do we know which jobs are pending? How do we do repeatables? Rate limiting per tenant? Proper backoff with visibility?"
The honest answer used to be: "you stand up Redis (or SQS) for that part."
@zintrust/queue-cloudflare is our attempt to make the honest answer better.
Two usage modes
1. Dead simple (no extra state)
import { Queue } from '@zintrust/core/queue';
await Queue.enqueue('EMAIL_QUEUE', { to: user.email, template: 'welcome' }, 'cloudflare');
This just uses Cloudflare Queues directly (via binding or the REST API). Great for fire-and-forget or when you already have your own tracking.
2. Full state layer (the interesting one)
import { CloudflareQueues } from '@zintrust/queue-cloudflare';
const queue = CloudflareQueues.create({
driver: 'cloudflare',
bindingName: 'EMAIL_QUEUE',
state: {
d1BindingName: 'QUEUE_DB',
kvBindingName: 'QUEUE_KV', // optional but recommended
coordinatorBindingName: 'QUEUE_COORDINATOR',
},
});
const job = await queue.add('email-jobs', 'send-welcome', data, {
attempts: 5,
backoff: { type: 'exponential', delay: 2000 },
delay: 60_000,
deduplication: { id: `welcome:${user.id}`, ttl: 86_400_000 },
});
await queue.getJobCounts('email-jobs', 'waiting', 'active', 'failed');
await job.updateProgress({ step: 'rendering' });
await job.log('template loaded', { templateId: 42 });
Under the hood:
- A tiny envelope (protocol + jobId + queue + attempt + availableAt) is what actually travels through Cloudflare Queues.
- The full job row, attempts, progress, result, error, logs, and flow links live in D1.
- Coordination (leases, heartbeats, rate limiting, pause gate) happens in a Durable Object with SQLite storage.
- The scheduler (delayed + repeatables + stalled reconciliation) runs from your cron trigger and reads/writes D1.
What you actually get
- Job lifecycle you can query and act on (
getJob,retry,promote,remove,clean) - Repeatable jobs via cron or fixed interval +
limit - Parent/child flows (
createFlow+ automatic parent release) - Per-job progress + structured logs
- Token-bucket rate limiting + queue pause/resume (enforced before dispatch)
- Stalled job recovery
- Retention policies (
removeOnComplete/removeOnFail) - Clean consumer helper that gives you
updateProgress,log, andheartbeatin context - DLQ integration (recommended)
- Same API surface whether you're running locally with
zin sor in Workers with--wg
The limits (read these)
Cloudflare Queues is an at-least-once system with its own constraints. We document the practical trade-offs directly so you don’t get surprised in production.
Important realities:
-
At-least-once. Your job processors must be idempotent. Use the
jobId,deduplication, and DLQ tooling we give you. - 128 KB maximum size for messages on Cloudflare Queues. Keep envelopes small. (limits)
- Retention on the queue itself is configurable up to 14 days (4 days default on paid plans; 24h non-configurable on the free plan). (limits, configuration)
- Scheduling and reconciliation run on whatever cron you configure (commonly every minute). You can add DO alarms for finer grain if you need it.
- Pause/resume is a dispatch gate. Already-delivered messages in a batch will still execute.
- You must provision and bind: the Queue(s), a D1, the coordinator Durable Object, a cron, and (strongly recommended) a DLQ.
- This is not Redis. Some things (exact priority ordering without multiple queues or scheduler dispatch, sandboxed processors, sub-second timing) are different by design.
If those constraints are acceptable, the upside is huge: you stay inside the Cloudflare billing and operational model instead of running a second stateful system whose only job is "be a queue."
Recommended minimal production wiring (wrangler.jsonc sketch)
{
"queues": {
"producers": [{ "queue": "email-jobs", "binding": "EMAIL_QUEUE" }],
"consumers": [{
"queue": "email-jobs",
"max_batch_size": 10,
"max_retries": 3,
"dead_letter_queue": "email-dlq"
}]
},
"d1_databases": [{ "binding": "QUEUE_DB", "database_name": "zintrust-queue" }],
"kv_namespaces": [{ "binding": "QUEUE_KV", "id": "..." }],
"durable_objects": {
"bindings": [{ "name": "QUEUE_COORDINATOR", "class_name": "CloudflareQueueCoordinator" }]
},
"migrations": [{ "tag": "queue-cloudflare-v1", "new_sqlite_classes": ["CloudflareQueueCoordinator"] }],
"triggers": { "crons": ["* * * * *"] }
}
Run the migration once:
zin migrate:queue-cloudflare --database zintrust-queue --remote
Then in your consumer Worker use the provided createConsumer helper.
When to use this vs something else
Use it when:
- The majority of your stack is already Cloudflare-native.
- You want observable, controllable background work without introducing Redis/SQS as a hard dependency.
- Your jobs are naturally idempotent or can be made so.
- 1-minute scheduling granularity is acceptable (the common case for notifications, reports, syncs, cleanups, etc.).
Reach for Redis/BullMQ or SQS instead when:
- You have hard sub-second or sub-10s scheduling SLAs.
- You need very high fan-out or per-queue throughput beyond current Cloudflare limits.
- You want a true drop-in replacement for an existing heavy BullMQ workload with minimal changes.
- Sandboxed / untrusted processor execution is a hard requirement.
Try it
npm install @zintrust/queue-cloudflare
# or inside a ZinTrust project:
zin add queue:cloudflare
The package is small, the surface is explicit, and the docs try very hard not to lie to you.
If you're doing real work on Cloudflare and queues are still the annoying exception in your architecture diagram, this might be worth an afternoon.
Feedback, real-world patterns, and "here's where it broke our assumptions" stories are extremely welcome in the repo or Discord.
The package README and https://zintrust.com/package-queue-cloudflare contain the full current guidance.
Package: https://www.npmjs.com/package/@zintrust/queue-cloudflare
Core docs: https://zintrust.com/package-queue-cloudflare
The package page includes setup, consumer patterns, wrangler examples, and the documented limitations.













