Skip to content
Get started
Get started

The SQLite Job Queue: Zero Infrastructure, No Redis

bunqueue v2.8.44 is here

bunqueue.
The queue is a file.

bunqueue is a zero-infrastructure job queue: priorities, retries, cron, rate limits and a dead letter queue in one process, persisted to a single SQLite file. No Redis, no broker, nothing to operate.

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 · no signup, no server to run · same API on 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

630K ops/sec bulk push, embedded90K ops/sec push over TCP5.5 MB install, 2 runtime deps500+ e2e scenarios across 7 runtimes
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()

same 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 · crates.io

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 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(); on the server, —data-path persists everything to a single SQLite file. Full quickstart · SDK guide

vs bullmq

Same job, three fewer boxes.

BullMQ is excellent software that requires Redis. bunqueue removes the requirement: the queue lives in your process and persists to one file. Coming from Celery, Sidekiq or asynq instead? Same trade: the broker box is the part that disappears.

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.db the 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.

Single-instance by design: one process owns the SQLite file. That is exactly what deletes the Redis box, and it draws a clear line. Read this before you adopt.

availability

No clustering, no failover

One server process owns the queue. Disaster recovery is S3 backup plus restore on a new host, not automatic failover. If the queue itself must span servers, BullMQ on Redis Cluster is the better fit, we say so in the comparison.

durability

Buffered writes by default

Writes are batched for up to 10ms, that buffer is what buys ~100K jobs/sec. A hard crash inside the window can lose those jobs. Jobs that cannot tolerate it take { durable: true } and hit disk before add() returns.

scaling

Workers scale, the server does not

Any number of worker processes on any machines connect over TCP, that is the scaling path. For edge and multi-site setups, run local embedded queues and drain them to a central server 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

One process. One file.

Point dataPath at bunq.db and everything the queue knows lives there: jobs and their states, schedules, results, the dead letter queue. Back it up with cp, inspect it with sqlite3, ship it to S3 on a schedule. Your infrastructure diagram loses three boxes, and there is no version skew between the queue and its store.

production/
├── app/                  your services
└── data/
└── bunq.db           the entire queue
· jobs            waiting · active · completed
· cron            schedules and repeatables
· dlq             failed jobs, kept
· results         return values
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}`));
capabilities

Four tools, one file.

Everything a production queue needs, in the same process and the same SQLite file. Adopt one piece or all of them, each stands on its own.

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, never lost on restart.

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

Workflow engine

Multi-step orchestration with automatic rollback when a step fails.

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

Operations

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

bunx bunqueue-dashboard
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 the write path is one batched transaction. Measured at 630K jobs/sec bulk push embedded and 90K over TCP, numbers and methodology here.

What happens on a crash?

Jobs live in SQLite, so a restart recovers waiting, delayed and active jobs. A worker that dies mid-job is caught by stall detection and its jobs are requeued. Jobs that keep failing land in the dead letter queue, kept and retryable.

Locks? “database is locked”?

Not in the supported topology: exactly one process opens the file, embedded in your app or as the server, and any number of workers connect over TCP. Two processes on one file is the single thing the docs tell you not to do.

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