From laptop to production.
This page shows the smallest working way to run bunqueue in production, then ready-to-paste configs for Docker, systemd, and PM2, plus health checks and backups.
bunqueue runs as a single process that stores everything in one SQLite file. There is no clustering and no multi-node setup to plan. Deploying it means answering one question: does the queue live inside your app process, or in its own process?
The smallest deploy: embedded mode
Section titled “The smallest deploy: embedded mode”Embedded mode means the queue runs inside your application process, no separate queue server at all. Point it at a file path so jobs survive restarts:
import { Queue, Worker } from 'bunqueue/client';
const queue = new Queue('emails', { embedded: true, dataPath: './data/bunq.db', // jobs persist here});
await queue.add('send', { to: 'user@example.com' });
new Worker('emails', async (job) => { await sendEmail(job.data); return { sent: true };}, { embedded: true, dataPath: './data/bunq.db', concurrency: 5 });Deploy your app the way you already deploy it. The queue ships with it. Done.
More than one process? Run the server
Section titled “More than one process? Run the server”The moment your API and your workers are separate processes (or separate containers), run the bunqueue server. One server owns the SQLite file; every other process talks to it over TCP:
bunqueue start --data-path ./data/bunq.db// api.ts, pushes jobs (no embedded flag = TCP client to localhost:6789)import { Queue } from 'bunqueue/client';const queue = new Queue('tasks');await queue.add('process', { data: '...' });// worker.ts, a separate process, restart and scale it independentlyimport { Worker } from 'bunqueue/client';new Worker('tasks', async (job) => { return { done: true };}, { concurrency: 10 });You can also push jobs without any SDK, via CLI or HTTP:
bunqueue push emails '{"to": "user@example.com"}'
curl -X POST http://localhost:6790/queues/emails/jobs \ -H "Content-Type: application/json" \ -d '{"data": {"to": "user@example.com"}}'Docker
Section titled “Docker”FROM oven/bun:1-alpine
WORKDIR /appCOPY package.json bun.lock* ./RUN bun install --frozen-lockfile --productionCOPY . .RUN mkdir -p /app/data
ENV DATA_PATH=/app/data/bunq.dbENV NODE_ENV=production
# wget ships with the Alpine base, curl does notHEALTHCHECK --interval=30s --timeout=3s \ CMD wget --spider -q http://127.0.0.1:6790/health || exit 1
EXPOSE 6789 6790CMD ["bun", "run", "start"]services: bunqueue: build: . ports: - "6789:6789" # TCP (SDK clients) - "6790:6790" # HTTP (health, metrics, REST) volumes: - bunqueue-data:/app/data environment: - DATA_PATH=/app/data/bunq.db - AUTH_TOKENS=${AUTH_TOKENS} - S3_BACKUP_ENABLED=1 - S3_ACCESS_KEY_ID=${S3_ACCESS_KEY_ID} - S3_SECRET_ACCESS_KEY=${S3_SECRET_ACCESS_KEY} - S3_BUCKET=${S3_BUCKET} - S3_REGION=${S3_REGION} restart: unless-stopped deploy: resources: limits: memory: 512M
volumes: bunqueue-data:systemd
Section titled “systemd”For bare-metal or VM deployments:
[Unit]Description=bunqueue Job QueueAfter=network.target
[Service]Type=simpleUser=bunqueueGroup=bunqueueWorkingDirectory=/var/lib/bunqueueExecStart=/usr/local/bin/bunqueue startRestart=alwaysRestartSec=5
Environment=NODE_ENV=productionEnvironment=DATA_PATH=/var/lib/bunqueue/bunq.dbEnvironmentFile=/etc/bunqueue.env
NoNewPrivileges=trueProtectSystem=strictProtectHome=trueReadWritePaths=/var/lib/bunqueue
MemoryMax=512M
[Install]WantedBy=multi-user.targetsudo systemctl daemon-reloadsudo systemctl enable --now bunqueuesudo journalctl -u bunqueue -fTo upgrade: stop the service, bun install -g bunqueue@latest, start it again, then confirm with curl http://localhost:6790/health | jq .version. Updating the package does not restart the service.
Build the standalone binary first (bun run build in the repo produces dist/bunqueue, a self-contained executable that bundles the Bun runtime), or install globally.
module.exports = { apps: [{ name: 'bunqueue', script: '/usr/local/bin/bunqueue', args: 'start', instances: 1, // single instance only, no cluster mode exec_mode: 'fork', autorestart: true, max_memory_restart: '512M', env: { NODE_ENV: 'production', DATA_PATH: '/var/lib/bunqueue/bunq.db', TCP_PORT: 6789, HTTP_PORT: 6790, }, }]};pm2 start ecosystem.config.jspm2 save && pm2 startup # survive rebootsHealth checks
Section titled “Health checks”The HTTP port (default 6790) serves everything an orchestrator needs:
curl http://localhost:6790/health # detailed: status, version, job counts, memorycurl http://localhost:6790/healthz # bare liveness, returns OKcurl http://localhost:6790/ready # readinesscurl http://localhost:6790/prometheus # metrics in Prometheus text format/health and /ready return 503 when persistent storage reports a full disk;
/healthz remains a pure process-liveness check. If METRICS_AUTH=true, also
configure AUTH_TOKENS and the scraper bearer token; otherwise /prometheus
fails closed with 503.
# KuberneteslivenessProbe: httpGet: { path: /healthz, port: 6790 } initialDelaySeconds: 5 periodSeconds: 10readinessProbe: httpGet: { path: /ready, port: 6790 } initialDelaySeconds: 5 periodSeconds: 5Alerting thresholds and dashboards are covered in Monitoring and Production Operations.
Backups and restore
Section titled “Backups and restore”The whole queue is one SQLite file, so back up that one file. The built-in S3 backup uploads periodic snapshots (works with AWS S3, Cloudflare R2, MinIO):
S3_BACKUP_ENABLED=1S3_BUCKET=my-bunqueue-backupsS3_ACCESS_KEY_ID=...S3_SECRET_ACCESS_KEY=...S3_REGION=us-east-1 # or S3_ENDPOINT for R2/MinIOS3_BACKUP_INTERVAL=3600000 # every hour (default 6h)S3_BACKUP_RETENTION=24 # snapshots keptThe persistent data path configured earlier is mandatory. The server flushes
its pending write buffer, then asks SQLite for a VACUUM INTO snapshot that
includes committed WAL frames; do not replace this with a raw live-file copy.
To restore after a disk loss:
systemctl stop bunqueuebunqueue backup listbunqueue backup restore backups/bunq-2026-01-30T12:00:00.db --forcesystemctl start bunqueueProvider-specific setup (R2, MinIO, Spaces) and how restore verifies SHA-256, checks SQLite integrity, and quarantines stale sidecars before replacing anything: S3 Backup guide.
Reference: key environment variables
Section titled “Reference: key environment variables”| Variable | What it does | Default |
|---|---|---|
BUNQUEUE_DATA_PATH | SQLite file path (aliases, in priority order: BQ_DATA_PATH, DATA_PATH, SQLITE_PATH) | in-memory |
TCP_PORT | Port for SDK clients | 6789 |
HTTP_PORT | Port for health, metrics, REST | 6790 |
AUTH_TOKENS | Comma-separated tokens clients must present | none (open) |
TLS_CERT_FILE / TLS_KEY_FILE | PEM cert and key for native TLS on TCP and HTTP. Set both or neither; setting only one is a startup error | off |
S3_BACKUP_ENABLED | Turn on S3 snapshots | 0 |
SHUTDOWN_TIMEOUT_MS | How long a graceful shutdown waits for active jobs | 30000 |
The full list, including all S3 and cloud variables, lives in Environment Variables. The same options are available as CLI flags: bunqueue start --tcp-port --http-port --data-path --auth-tokens --tls-cert --tls-key.
Sizing
Section titled “Sizing”bunqueue is I/O bound, not CPU bound; one core handles most workloads.
| Jobs per day | RAM |
|---|---|
| under 10k | 128-256 MB |
| 10k-100k | 512 MB |
| over 100k | 1 GB+ |
Disk grows with retained jobs. Set removeOnComplete: true in job options to drop completed jobs, and keep removeOnFail: false so failures stay inspectable. Scale vertically: more RAM, faster disk (NVMe), higher worker concurrency.
Gotchas
Section titled “Gotchas”- No horizontal scaling. No multi-node, no failover, no distributed processing. If you need those, reach for Redis + BullMQ with Sentinel, Kafka, or a managed queue like SQS. Multiple worker processes on one machine are fine, through the TCP server.
- Two embedded processes, one file, is silent corruption. See the warning above; always go through the server for multi-process.
- Copying the database while the server runs can produce a torn copy. SQLite runs in WAL mode (writes go to a
.db-walsidecar file first). Stop the server, then copybunq.dbandbunq.db-waltogether, or just use the built-in S3 backup, which snapshots safely while live. AUTH_TOKENSis not set by default. Anyone who can reach the ports can push and pull jobs. Set tokens (openssl rand -hex 32) and either enable native TLS or keep the ports on a private network. See Security.- Jobs you cannot afford to lose in a hard crash should be
durable: true. By default writes are buffered for up to 10 ms for throughput;durablewrites hit disk before the add returns. Details in Production Operations.