Skip to content
Get started
Get started
bunqueue Code Examples: Copy-Paste Recipes for Bun
View Markdown
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 starts with one local job, adds reliability and operational controls, then finishes with workflows and a tested PostgreSQL multi-broker deployment. For domain scenarios such as email, webhooks, and payments, see use cases.

Follow the stages in order on a first read. Each stage links directly to the relevant recipe, so you can return later and use the page as a reference.

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.

Every job starts with a producer, waits until it is eligible, and is claimed by one worker. A successful acknowledgement completes it. A failure either schedules another attempt after backoff or moves the job to the dead letter queue when no attempt remains.

Use the controls to compare the success, retry, and terminal-failure routes one transition at a time. The same state rules apply in embedded, SQLite, and PostgreSQL deployments.

Interactive lifecycle

Follow one job, one state at a time

Select an outcome, then advance the state.
  1. Producer queue.add() persists the job and returns its ID.
  2. Ready queue The job is eligible and waits in scheduling order.
  3. Worker One worker claims the job and owns its active attempt.
  4. Completed The ACK saves the result and releases the concurrency slot.
1 / 4

Succeeds first time. Step 1 of 4: queue.add() persists the job and returns its ID.

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 the selected durable backend and survive restarts; PostgreSQL mode coordinates named schedules across brokers.

// 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

Start embedded while one process is the right boundary. Introduce a TCP broker when producers and workers need separate processes or different languages. Add PostgreSQL and multiple active brokers only when broker failover, horizontal scale, or shared cross-host limits justify the extra moving parts.

Interactive topology

Change only the boundary you need

The Queue and Worker API stays familiar.
Best fit
The smallest deployment, local services, and edge processes.
Durability owner
Memory or one local SQLite file.

Embedded: The producer, queue runtime, and worker share one Bun process.

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; 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.

The complete project below combines the earlier concepts. Read it after the single-broker examples if this is your first bunqueue deployment.

PostgreSQL multi-broker

Run PostgreSQL 18.6, three active brokers, multiple queues and workers, authenticated metrics, custom-ID idempotency, retries, DLQ recovery, shared limits, events, and durable flows. Every source is executed in disposable containers and has a published engineering report.

Open the complete example →