Skip to content
Get started
Get started
bunqueue Code Examples: Copy-Paste Recipes for Bun
reference · examples

Code examples you copy and ship.

Short recipes for the tasks you hit first: retries, schedules, dedup, events, shutdown and workflows. Each one links to the guide that covers it in depth.

This page is a set of small, working snippets. For end-to-end scenarios like email pipelines, webhooks and payments, see use cases.

The smallest complete setup: add a job, process it in the background.

import { Queue, Worker } from 'bunqueue/client';
const queue = new Queue('tasks', { embedded: true, dataPath: './data/bunq.db' });
const worker = new Worker('tasks', async (job) => {
console.log('processing', job.data);
return { done: true };
}, { embedded: true, concurrency: 5 });
await queue.add('hello', { message: 'world' });

More in the quickstart.

A thrown error retries the job with backoff, a growing delay between attempts. Jobs that run out of attempts land in the dead letter queue (DLQ), a holding area you can inspect and retry.

await queue.add('flaky-call', { url: 'https://api.example.com' }, {
attempts: 5, // try up to 5 times
backoff: 2000, // wait 2s, 4s, 8s... between tries
});
// After all attempts fail:
const failed = queue.getDlq(); // inspect what died and why
queue.retryDlq(); // send everything back for another run

Details and auto-retry config in the DLQ guide.

Attach a repeat option, or use upsertJobScheduler() for named schedules. Both persist in SQLite and survive restarts.

// Cron expression: every day at 6 AM
await queue.add('daily-report', { type: 'sales' }, {
repeat: { pattern: '0 6 * * *' },
});
// Plain interval: every 30 minutes
await queue.add('health-check', {}, {
repeat: { every: 1_800_000 },
});
// Named, updatable schedule
await queue.upsertJobScheduler('cleanup', { pattern: '0 3 * * *' }, {
data: { olderThanDays: 30 },
});

Timezones and schedule management in the cron guide.

Adding a job with a jobId that already exists returns the existing job instead of creating a duplicate. Useful for “exactly one welcome email per user” and safe re-runs after a restart.

const a = await queue.add('notify', { userId: 'u1' }, { jobId: 'welcome-u1' });
const b = await queue.add('notify', { userId: 'u1' }, { jobId: 'welcome-u1' });
console.log(a.id === b.id); // true, same job

Run one bunqueue server, connect producers and workers from any number of processes or machines, in any language.

Terminal window
bunqueue start --tcp-port 6789 --data-path ./data/tasks.db
producer.ts
import { Queue } from 'bunqueue/client';
const queue = new Queue('tasks', { connection: { host: 'localhost', port: 6789 } });
await queue.addBulk(items.map((i) => ({ name: 'process', data: i })));
// worker.ts (run as many copies as you want)
import { Worker } from 'bunqueue/client';
new Worker('tasks', async (job) => {
return { processed: job.data.id };
}, { connection: { host: 'localhost', port: 6789 }, concurrency: 50 });

Server setup, auth and TLS in the server guide.

QueueEvents streams lifecycle events for a queue, and workers emit their own events.

import { QueueEvents } from 'bunqueue/client';
const events = new QueueEvents('tasks', {
connection: { host: '127.0.0.1', port: 6789 },
});
await events.waitUntilReady();
events.on('completed', ({ jobId, returnvalue }) => console.log('done', jobId, returnvalue));
events.on('failed', ({ jobId, failedReason }) => console.error('failed', jobId, failedReason));
events.on('progress', ({ jobId, data }) => console.log('progress', jobId, data));
worker.on('completed', (job, result) => console.log('worker finished', job.id));
worker.on('failed', (job, error) => console.error('worker error', error.message));

QueueEvents streaming is available in the Bun bunqueue package only. Rust and Elixir surface lifecycle through the structured telemetry callback instead of worker listeners, see the SDK guide.

Dashboards, metrics and Prometheus in the monitoring guide.

On SIGTERM, stop pulling new jobs, let active ones finish, then close.

async function shutdown() {
worker.pause(); // stop accepting new jobs
await worker.close(); // wait for active jobs (worker.close(true) forces a stop)
await queue.close();
process.exit(0);
}
process.on('SIGTERM', shutdown);
process.on('SIGINT', shutdown);

The full production pattern, including timeouts and the embedded manager, is in the production guide.

The workflow engine runs multi-step processes where each step can declare a compensate function, code that undoes the step if a later one fails. This is the saga pattern: charge succeeded but shipping failed, so the charge is refunded automatically.

The workflow engine ships with the Bun bunqueue package (bunqueue/workflow) and runs embedded. From the other SDKs, use flows for multi-step orchestration against the server.

import { Workflow, Engine } from 'bunqueue/workflow';
const orderFlow = new Workflow('order')
.step('reserve-stock', async (ctx) => {
await inventory.reserve((ctx.input as { orderId: string }).orderId);
return { reserved: true };
}, {
compensate: async () => { await inventory.release(); }, // runs if a later step fails
})
.step('charge', async (ctx) => {
const txId = await stripe.charge((ctx.input as { amount: number }).amount);
return { txId };
}, {
compensate: async () => { await stripe.refund(); },
})
.step('confirm', async (ctx) => {
const { txId } = ctx.steps['charge'] as { txId: string };
await mailer.send('order-confirm', { txId });
return { done: true };
});
const engine = new Engine({ embedded: true });
engine.register(orderFlow);
await engine.start('order', { orderId: 'ORD-1', amount: 99.99 });

waitFor() pauses the workflow until someone calls engine.signal(), hours or days later.

import { Workflow, Engine } from 'bunqueue/workflow';
const expenseFlow = new Workflow('expense')
.step('submit', async (ctx) => {
await slack.notify('#approvals', `New expense: ${JSON.stringify(ctx.input)}`);
return { submitted: true };
})
.waitFor('manager-decision')
.step('process', async (ctx) => {
const decision = ctx.signals['manager-decision'] as { approved: boolean };
return { status: decision.approved ? 'paid' : 'rejected' };
});
const engine = new Engine({ embedded: true });
engine.register(expenseFlow);
const run = await engine.start('expense', { amount: 500 });
// Later, when the manager clicks approve:
await engine.signal(run.id, 'manager-decision', { approved: true });

Branching, parallel steps, loops, sub-workflows and schema validation are all in the workflow guide.