Skip to content
Get started
Get started
The Bun Job Queue: SQLite or PostgreSQL, No Redis
bunqueue v2.9.2 is here

bunqueue.
One file. Many brokers.

bunqueue is zero-infrastructure background work by default: priorities, retries, cron, a dead letter queue, a durable workflow engine, and a native MCP server. Use one process in memory or with one SQLite file, or opt into PostgreSQL 15–18 when several brokers must share one authoritative queue; 18.6 is recommended. No Redis required. The queue speaks to six languages; the workflow engine runs in-process on Bun.

1 · Start the server (skip on Bun: it runs embedded)

2 · Install the client, in your language

Terminal window
bun add bunqueue # embedded: queue + worker in your process

MIT licensed · embedded or standalone · protocol-conformant Queue/Worker/Flow core across Node.js, Deno, Bun, Python, PHP, Go, Rust, Elixir and Cloudflare Workers

live simulation: higher priority jobs jump the line, failures retry with backoff, exhausted retries land in the dead letter queue

186K jobs/sec public on-disk addBulk, embedded159K jobs/sec PUSHB over TCPPostgreSQL 15–18 multi-broker1 runtime dependency500+ e2e scenarios across 7 runtimes
capabilities

Not just a queue.

A queue is where bunqueue starts, not where it stops. Use memory by default, configure one SQLite file for local persistence, or choose an authoritative PostgreSQL database for a broker fleet. The server also provides cron, flows, a dead letter queue, shared operations, and a native MCP server for AI agents. Every official client shares the protocol-conformant Queue/Worker/Flow core; language-specific capabilities are listed in the SDK matrix. The workflow engine and its agent integrations are in-process Bun APIs.

Queue & Worker

BullMQ-familiar API for adding and processing jobs, end to end TypeScript.

await queue.add('welcome', data, { priority: 5 })

Cron & schedulers

Repeatable jobs with cron patterns, intervals and timezones, persisted across restarts when SQLite or PostgreSQL storage is configured.

await app.cron('daily', '0 9 * * *', data)

Workflow engine

Multi-step orchestration with automatic rollback when a step fails. In-process, Bun only.

new Workflow('order').step(...).parallel(...)

Operations

Failed jobs are kept and retryable, metrics are scrapable, backups are scheduled.

bunx bunqueue-dashboard

AI agents

A native MCP server, usable from any language, plus durable agent runs on Bun that survive a restart and roll back the tools a model called.

bunx --package=bunqueue bunqueue-mcp

Polyglot & edge

One server, six official client languages, and a store-and-forward mode that drains edge jobs to a central instance.

queue.forward({ to: { host: 'central' } })
polyglot clients

One queue, any language.

The server owns every queue semantic, so clients stay thin and identical in spirit: add a job in one language, process it in another, same options, same guarantees. Official SDKs for TypeScript, Python, PHP, Go, Rust and Elixir, built on a formal wire protocol.

SDK guidenpm

TypeScript · Node, Deno, Bun

import { Queue, Worker } from 'bunqueue-client';
const queue = new Queue('emails');
await queue.add('welcome', { to: 'a@b.co' });
new Worker(
'emails',
async (job) => {
await sendEmail(job.data);
return { sent: true };
},
{ concurrency: 10 }
);

npm install bunqueue-client · Node, Deno, Bun and Workers

Python

from bunqueue import Queue, Worker
queue = Queue("emails")
queue.add("welcome",
{"to": "a@b.co"}, attempts=3)
def process(job):
send_email(job.data["to"])
return {"sent": True}
Worker("emails", process,
concurrency=10).run()

familiar API, snake_case · sync producer, threaded workers

PHP

use Bunqueue\Queue;
use Bunqueue\Worker;
$queue = new Queue('emails');
$queue->add('welcome',
['to' => 'a@b.co'],
['attempts' => 3]);
$worker = new Worker('emails',
fn ($j) => sendEmail($j->data()));
$worker->run(); // or runOnce()

FPM-friendly producer, CLI worker · runOnce() for cron ticks

Go

queue := bunqueue.NewQueue("emails",
bunqueue.Options{})
queue.Add("welcome",
map[string]any{"to": "a@b.co"}, nil)
worker := bunqueue.NewWorker("emails",
func(job *bunqueue.Job) (any, error) {
return sendEmail(job.Data())
}, bunqueue.WorkerOptions{
Concurrency: 8,
})
worker.Run()

goroutine worker pool · one dependency, go get and go

Rust

use bunqueue_client::{Queue, Worker,
ConnectionOptions, WorkerOptions,
JobOptions};
let queue = Queue::new("emails",
ConnectionOptions::default());
queue.add("welcome", data,
JobOptions::default())?;
let worker = Worker::new("emails",
|job| process(job.data()),
WorkerOptions::default());
worker.run()?;

typed options, no insecure TLS mode · API docs

Elixir

queue = Bunqueue.queue("emails")
{:ok, _job} = Bunqueue.Queue.add(queue,
"welcome", %{to: "a@b.co"})
worker =
Bunqueue.Worker.new("emails", fn job ->
send_email(job.data)
{:ok, %{sent: true}}
end, concurrency: 8)
Bunqueue.Worker.run(worker)

tagged tuples, OTP-friendly · Hex upcoming

Cloudflare Workers

import { Queue } from 'bunqueue-client';
export default {
async fetch(req: Request, env: Env) {
const q = new Queue('signups', { host: env.HOST, tls: true });
const job = await q.add('welcome', await req.json());
q.close();
return Response.json({ queued: job.id });
},
};

nodejs_compat flag, produce from fetch handlers · consume via Cron Triggers

CLI & AI agents

Terminal window
# push, watch, inspect from the terminal
bunqueue push emails '{"to":"a@b.co"}'
bunqueue stats
bunqueue dlq list emails
# or let an agent drive it: 73 MCP tools
claude mcp add bunqueue \
-- bunx --package=bunqueue bunqueue-mcp

MCP server for Claude, Cursor and any MCP client

quickstart

Write ten lines. Run.

The same ten lines in every language: a queue, a worker, a job. On Bun it all runs embedded in your process; from any other language the code talks to the server you started with one command.

1

Write a queue and a worker

import { Queue, Worker } from 'bunqueue/client';
const queue = new Queue('emails', { embedded: true });
new Worker(
'emails',
async (job) => {
console.log(`to ${job.data.to}`);
return { sent: true };
},
{ embedded: true }
);
await queue.add('welcome', { to: 'a@b.co' }, { attempts: 3 });
2

Run

Terminal window
bun app.ts # no server: embedded
# to a@b.co

That’s the whole setup, in any of the six languages. Retries, priorities, cron, rate limits and the dead letter queue are options on add(). Use —data-path for a single SQLite-backed server, or BUNQUEUE_POSTGRES_URL when several servers must share one queue. Full quickstart · Storage guide · SDK guide

vs bullmq

Same job, three fewer boxes.

BullMQ is excellent software that requires Redis. bunqueue removes that requirement: run the queue in your process and persist it to one SQLite file, or reuse PostgreSQL when you need several active brokers. Coming from Celery, Sidekiq or asynq instead? The public queue contract stays familiar while Redis remains optional infrastructure you do not need.

Running BullMQ

your app bullmq client

redis server provision · secure · monitor

redis persistence AOF / RDB tuning

redis upgrades versions · memory limits

4 moving partsone of them stateful, on call

Running bunqueue

your app bunqueue / bunqueue-client

bunq.dbthe entire queue

1 filecp to back up, sqlite3 to inspect

Pushing 100-job batches over TCP

throughput in ops/sec vs BullMQ + Redis, identical workloads

bunqueue · 85,700 ops/s3.5x
BullMQ + Redis · 24,800 ops/s

p99 push latency, lower is better

bunqueue · 6.3 ms1.8x lower
BullMQ + Redis · 11.1 ms

Apple M1 Max · Bun 1.3.14 · BullMQ 5.79.3 · Redis 8.8.0 · methodology · all benchmarks →

trade-offs

Where bunqueue stops.

SQLite keeps the smallest topology; PostgreSQL 15–18 adds a multi-broker topology, with 18.6 recommended. Each has a clear operational boundary. Read this before you adopt.

availability

Storage owns availability

SQLite is one broker with snapshot restore. PostgreSQL mode supports several brokers, while database HA, routing and PITR remain yours to operate. Neither mode is a multi-region consensus layer. See the storage contract.

durability

SQLite lets you choose

SQLite batches non-durable writes for up to 10ms; a process crash inside that window can lose them. Jobs that cannot tolerate bunqueue’s buffer take { durable: true } and commit before add() returns. PostgreSQL admissions are transactional and do not use this buffer; machine/power-loss durability remains a storage-operator concern.

scaling

Workers and brokers can scale

Any number of workers connect over TCP. The SQLite server stays single; PostgreSQL 15–18 coordinates multiple broker processes through transactional claims and fenced leases. Edge queues can still drain with forward().

dashboard · beta

See the queue move.

A web dashboard that fully drives your server: queues, jobs, DLQ, cron, webhooks, workers, live activity, a SQLite inspector and an AI copilot. One command: bunx bunqueue-dashboard.

Open the live demo

persistence

Start in one file. Scale into shared SQL.

Choose the operational boundary that fits today. Storage selection changes the deployment topology, not the client API or job lifecycle.

default

SQLite · one broker

Point dataPath at bunq.db. Jobs, schedules, results and the DLQ live in one inspectable file, with no external service and built-in S3 snapshots.

multi-broker

PostgreSQL 15–18

Several standalone servers share transactional claims, fenced leases, durable events, limits, cron, workers, job-state/lifecycle metrics, flows and DLQ state. PostgreSQL 18.6 is recommended.

native Bun

No ORM. No Redis.

The PostgreSQL path uses Bun’s built-in SQL pool and prepared tagged templates directly. SQLite keeps its synchronous bun:sqlite hot path.

developer experience

A queue and a worker, ten lines.

The same familiar Queue and Worker API in six languages, and 3.5x the bulk push throughput of BullMQ measured on identical workloads, methodology below. Migrating takes minutes, the mental model is the same.

import { Queue, Worker } from 'bunqueue/client';
const queue = new Queue('emails', { embedded: true });
await queue.add('welcome', { to: 'a@b.co' }, { attempts: 3 });
const worker = new Worker(
'emails',
async (job) => {
await sendEmail(job.data);
return { sent: true };
},
{ embedded: true, concurrency: 5 }
);
worker.on('completed', (job, result) => console.log(`done: ${job.id}`));
trust

Verified, secured, observable.

quality

Tested on every runtime

  • 110 e2e scenarios each on Node.js, Deno, Bun
  • 16 scenarios inside workerd
  • 100 in Python · 33 in PHP · 32 in Go
  • every public SDK method covered
security

Hardened by default

operations

Observable in production

  • Prometheus metrics endpoint
  • health and readiness probes
  • webhooks, SSE and WebSocket events
  • S3 backups for disaster recovery
sqlite, seriously?

The questions everyone asks.

Can SQLite handle a production queue?

In WAL mode, yes: reads and writes overlap and persistence is batched. A native repeated-process campaign measured 186K jobs/sec public on-disk Embedded addBulk and 159K TCP PUSHB; the separate 729K result is an internal in-memory batch path. Numbers, distributions and methodology here.

What happens on a crash?

With SQLite or PostgreSQL persistence configured, a restart recovers waiting, delayed and active work that was durably admitted. A worker that dies mid-job is caught by stall detection; PostgreSQL mode additionally fences the old lease before another broker recovers it. Repeated failures land in the retryable dead letter queue.

Can I run multiple brokers?

Yes, with PostgreSQL 15–18. Independent servers claim jobs with FOR UPDATE SKIP LOCKED, coordinate through database-clock leases and replay missed notifications from a durable event journal. Do not point multiple processes at one SQLite file.

Start in under a minute.

One install, ten lines, zero infrastructure. On the runtime you already use.

If bunqueue removes a Redis box from your stack, a star on GitHub helps other teams find it.

Blog · Simulator · vs BullMQ · FAQ · Docs