- Docs
- Start Here
- Quick Start
Working queue in a minute.
From nothing to a processed job in about a minute, in Node.js, Bun, Deno, Python, PHP, Go, Rust or Elixir. Copy, run, see it work, then add retries, persistence and the rest.
Run your first job
Section titled “Run your first job”Pick how you run bunqueue. Both ways are free and have the same queue features.
-
Start the server. With Docker running, no Bun needed:
docker run -d --name bunqueue \--restart unless-stopped \-p 127.0.0.1:6789:6789 \-p 127.0.0.1:6790:6790 \-v bunqueue-data:/app/data \egeominotti/bunqueue:alpineOr, with Bun installed, run it directly:
bunx bunqueue start --host 127.0.0.1 --data-path ./bunqueue.db. Check that it is ready:curl --fail http://127.0.0.1:6790/health -
Add a job and process it. In another terminal, install the client for your language, save the file and run it.
npm install bunqueue-clientjobs.mjs import { Queue, Worker } from 'bunqueue-client';const options = {embedded: false,connection: { host: '127.0.0.1', port: 6789 },};const queue = new Queue('emails', options);const worker = new Worker('emails',async (job) => {console.log('Processing:', job.data.to);return { sent: true };},options);worker.on('error', (error) => console.error(error));await queue.add('welcome', { to: 'hello@example.com' });node jobs.mjsbun add bunqueue-clientjobs.mjs import { Queue, Worker } from 'bunqueue-client';const options = {embedded: false,connection: { host: '127.0.0.1', port: 6789 },};const queue = new Queue('emails', options);const worker = new Worker('emails',async (job) => {console.log('Processing:', job.data.to);return { sent: true };},options);worker.on('error', (error) => console.error(error));await queue.add('welcome', { to: 'hello@example.com' });bun jobs.mjsdeno add npm:bunqueue-clientjobs.ts import { Queue, Worker } from 'bunqueue-client';const options = {embedded: false,connection: { host: '127.0.0.1', port: 6789 },};const queue = new Queue<{ to: string }>('emails', options);const worker = new Worker<{ to: string }>('emails',async (job) => {console.log('Processing:', job.data.to);return { sent: true };},options);worker.on('error', (error) => console.error(error));await queue.add('welcome', { to: 'hello@example.com' });deno run --allow-net --allow-env --allow-read --allow-sys jobs.tspip install bunqueue-clientjobs.py from bunqueue import Queue, Workerconnection = {"host": "127.0.0.1", "port": 6789}queue = Queue("emails", **connection)queue.add("welcome", {"to": "hello@example.com"})queue.close()def process(job):print("Processing:", job.data["to"])return {"sent": True}Worker("emails", process, **connection).run()python jobs.pycomposer require bunqueue/client # PHP 8.1+ and Composerjobs.php <?phprequire __DIR__ . '/vendor/autoload.php';use Bunqueue\Job;use Bunqueue\Queue;use Bunqueue\Worker;$connection = ['host' => '127.0.0.1', 'port' => 6789];$queue = new Queue('emails', $connection);$queue->add('welcome', ['to' => 'hello@example.com']);$queue->close();$worker = new Worker('emails', function (Job $job) {echo 'Processing: ', $job->data()['to'], PHP_EOL;return ['sent' => true];}, $connection);$worker->run();php jobs.phpgo mod init jobs && go get github.com/egeominotti/bunqueue/sdk/go # Go 1.26.5+main.go package mainimport ("fmt"bunqueue "github.com/egeominotti/bunqueue/sdk/go")func main() {queue := bunqueue.NewQueue("emails", bunqueue.Options{Host: "127.0.0.1", Port: 6789})defer queue.Close()if _, err := queue.Add("welcome", map[string]any{"to": "hello@example.com"}, nil); err != nil {panic(err)}worker := bunqueue.NewWorker("emails", func(job *bunqueue.Job) (any, error) {fmt.Println("Processing:", any(job.Data()).(map[string]any)["to"])return map[string]any{"sent": true}, nil}, bunqueue.WorkerOptions{Host: "127.0.0.1", Port: 6789})worker.Run()}go run .cargo new jobs && cd jobs && cargo add bunqueue-clientsrc/main.rs use bunqueue_client::{ConnectionOptions, JobOptions, Queue, Value, Worker, WorkerOptions};fn main() -> Result<(), Box<dyn std::error::Error>> {let connection = ConnectionOptions {host: "127.0.0.1".into(),port: 6789,..Default::default()};let queue = Queue::new("emails", connection.clone());let data = Value::Map(vec![(Value::from("to"), Value::from("hello@example.com"))]);queue.add("welcome", data, JobOptions::default())?;let worker = Worker::new("emails",|job| {println!("Processing: {}", job.data()["to"].as_str().unwrap_or_default());Ok(Value::Map(vec![(Value::from("sent"), Value::from(true))]))},WorkerOptions { connection, ..Default::default() },);worker.run()?;Ok(())}cargo rungit clone --depth 1 https://github.com/egeominotti/bunqueue && mix new jobs && cd jobsmix.exs # Replace the generated deps/0. Hex release upcoming: until then, use the# bunqueue checkout next to this project as a path dependency.defp deps do[{:bunqueue_client, path: "../bunqueue/sdk/elixir"}]endjobs.exs connection = [host: "127.0.0.1", port: 6789]queue = Bunqueue.queue("emails", connection){:ok, _job} = Bunqueue.Queue.add(queue, "welcome", %{to: "hello@example.com"})worker =Bunqueue.Worker.new("emails",fn job ->IO.puts("Processing: #{job.data["to"]}"){:ok, %{sent: true}}end,connection: connection)Bunqueue.Worker.run(worker)mix local.hex --force && mix deps.get && mix run jobs.exs -
See it work. The worker prints the job your app just added, then keeps waiting for more (press Ctrl+C to stop it):
Processing: hello@example.com
-
Install bunqueue. The queue runs inside your app, so there is no server to start.
bun add bunqueue -
Add a job and process it. Save the file and run it.
jobs.ts import { Queue, Worker } from 'bunqueue/client';const queue = new Queue<{ to: string }>('emails', { embedded: true });const worker = new Worker<{ to: string }>('emails',async (job) => {console.log('Processing:', job.data.to);return { sent: true };},{ embedded: true });worker.on('error', (error) => console.error(error));await queue.add('welcome', { to: 'hello@example.com' });bun jobs.ts -
See it work. The worker prints the job, then keeps waiting for more (press Ctrl+C to stop it):
Processing: hello@example.com
It works. Now pick what you need
Section titled “It works. Now pick what you need”| You want to | Read |
|---|---|
| Retry failed jobs and see why they failed | Errors & retries, Dead letter queue |
| Run something every hour or every day | Cron |
| Keep jobs when the process restarts | Turn on persistence |
| Watch queues and jobs in a browser | Web dashboard |
| Let an AI agent manage the queue | MCP server |
| Run jobs in a chain or a tree | Flows, Workflow engine |
Prefer to watch? The first bunqueue Academy episode walks through the same steps.
The rest of this page builds on the same queue and worker: job options, the processor, events, persistence and a clean shutdown.
Add jobs with options
Section titled “Add jobs with options”// Typed queue: job.data is type-checkedinterface EmailJob { to: string; subject: string;}const emailQueue = new Queue<EmailJob>('emails', { embedded: true });
// Priority, delay, retriesawait emailQueue.add( 'send-email', { to: 'a@test.com', subject: 'Hi' }, { priority: 10, // Higher = processed first delay: 5000, // Wait 5 seconds before processing attempts: 3, // At most 3 attempts (2 retries) if the processor throws backoff: 1000, // Wait 1 second between retries (grows on each attempt) });
// Many jobs at once (one optimized batch)await emailQueue.addBulk([ { name: 'send-email', data: { to: 'a@test.com', subject: 'Hi' } }, { name: 'send-email', data: { to: 'b@test.com', subject: 'Hi' } },]);// Typed queue: job.data is type-checkedinterface EmailJob { to: string; subject: string;}const emailQueue = new Queue<EmailJob>('emails', { embedded: false });
// Priority, delay, retriesawait emailQueue.add( 'send-email', { to: 'a@test.com', subject: 'Hi' }, { priority: 10, // Higher = processed first delay: 5000, // Wait 5 seconds before processing attempts: 3, // At most 3 attempts (2 retries) if the processor throws backoff: 1000, // Wait 1 second between retries (grows on each attempt) });
// Many jobs at once (one optimized batch)await emailQueue.addBulk([ { name: 'send-email', data: { to: 'a@test.com', subject: 'Hi' } }, { name: 'send-email', data: { to: 'b@test.com', subject: 'Hi' } },]);# Priority, delay, retriesqueue.add("send-email", {"to": "a@test.com", "subject": "Hi"}, priority=10, # Higher = processed first delay=5000, # Wait 5 seconds before processing attempts=3, # At most 3 attempts (2 retries) if the processor raises backoff=1000) # Wait 1 second between retries (grows on each attempt)
# Many jobs at once (one optimized batch)queue.add_bulk([ {"name": "send-email", "data": {"to": "a@test.com", "subject": "Hi"}}, {"name": "send-email", "data": {"to": "b@test.com", "subject": "Hi"}},])// Priority, delay, retries$queue->add('send-email', ['to' => 'a@test.com', 'subject' => 'Hi'], [ 'priority' => 10, // Higher = processed first 'delay' => 5000, // Wait 5 seconds before processing 'attempts' => 3, // At most 3 attempts (2 retries) if the processor throws 'backoff' => 1000, // Wait 1 second between retries (grows on each attempt)]);
// Many jobs at once (one optimized batch)$queue->addBulk([ ['name' => 'send-email', 'data' => ['to' => 'a@test.com', 'subject' => 'Hi']], ['name' => 'send-email', 'data' => ['to' => 'b@test.com', 'subject' => 'Hi']],]);// Priority, delay, retriesqueue.Add("send-email", map[string]any{"to": "a@test.com", "subject": "Hi"}, bunqueue.JobOptions{ "priority": 10, // Higher = processed first "delay": 5000, // Wait 5 seconds before processing "attempts": 3, // At most 3 attempts (2 retries) if the processor errors "backoff": 1000, // Wait 1 second between retries (grows on each attempt) })
// Many jobs at once (one optimized batch)ids, err := queue.AddBulk([]bunqueue.BulkEntry{ {Name: "send-email", Data: map[string]any{"to": "a@test.com", "subject": "Hi"}}, {Name: "send-email", Data: map[string]any{"to": "b@test.com", "subject": "Hi"}},})use bunqueue_client::{Backoff, BulkEntry, JobOptions, Value};
let data = Value::Map(vec![ (Value::from("to"), Value::from("a@test.com")), (Value::from("subject"), Value::from("Hi")),]);
// Priority, delay, retriesqueue.add("send-email", data.clone(), JobOptions { priority: Some(10), // Higher = processed first delay: Some(5000), // Wait 5 seconds before processing attempts: Some(3), // At most 3 attempts (2 retries) backoff: Some(Backoff::Milliseconds(1000)), // Wait 1 second between retries ..Default::default()})?;
// Many jobs at once (one optimized batch)queue.add_bulk(vec![ BulkEntry { name: "send-email".into(), data: data.clone(), options: JobOptions::default() }, BulkEntry { name: "send-email".into(), data, options: JobOptions::default() },])?;# Priority, delay, retries{:ok, _job} = Bunqueue.Queue.add(queue, "send-email", %{to: "a@test.com", subject: "Hi"}, # Higher = processed first; wait 5s; at most 3 attempts; 1s between retries priority: 10, delay: 5000, attempts: 3, backoff: 1000 )
# Many jobs at once (one optimized batch){:ok, _ids} = Bunqueue.Queue.add_bulk(queue, [ %{name: "send-email", data: %{to: "a@test.com", subject: "Hi"}}, %{name: "send-email", data: %{to: "b@test.com", subject: "Hi"}} ])All options are in the Queue guide.
Do more inside the processor
Section titled “Do more inside the processor”const worker = new Worker<EmailJob>( 'emails', async (job) => { await job.updateProgress(50, 'Sending email...'); // Report progress await sendEmail(job.data); // Do the work await job.log('Email sent successfully'); // Attach a log line return { sent: true, timestamp: Date.now() }; // Result, stored and queryable }, { embedded: true, concurrency: 5, // Process 5 jobs in parallel });const worker = new Worker<EmailJob>( 'emails', async (job) => { await job.updateProgress(50, 'Sending email...'); // Report progress await sendEmail(job.data); // Do the work await job.log('Email sent successfully'); // Attach a log line return { sent: true, timestamp: Date.now() }; // Result, stored and queryable }, { embedded: false, concurrency: 5, // Process 5 jobs in parallel });def process(job): job.update_progress(50, "Sending email...") # Report progress send_email(job.data) # Do the work job.log("Email sent successfully") # Attach a log line return {"sent": True} # Result, stored and queryable
Worker("emails", process, concurrency=5).run() # Process 5 jobs in parallel$worker = new Worker('emails', function (Bunqueue\Job $job) { $job->updateProgress(50, 'Sending email...'); // Report progress sendEmail($job->data()); // Do the work $job->log('Email sent successfully'); // Attach a log line return ['sent' => true]; // Result, stored and queryable});$worker->run(); // The PHP worker is sequential by design (one job at a time)worker := bunqueue.NewWorker("emails", func(job *bunqueue.Job) (any, error) { job.UpdateProgress(50, "Sending email...") // Report progress if err := sendEmail(job.Data()); err != nil { // Do the work return nil, err } job.Log("Email sent successfully", "info") // Attach a log line return map[string]any{"sent": true}, nil // Result, stored and queryable}, bunqueue.WorkerOptions{Concurrency: 5}) // Process 5 jobs in paralleluse bunqueue_client::{ProcessError, Value, Worker, WorkerOptions};
let worker = Worker::new( "emails", |job| { let _ = job.update_progress(50.0, Some("Sending email...")); // Report progress send_email(job.data()) // Do the work .map_err(|e| ProcessError::retryable(e.to_string()))?; let _ = job.log("Email sent successfully", None); // Attach a log line Ok(Value::from(true)) // Result, stored }, WorkerOptions { concurrency: 5, ..Default::default() }, // 5 jobs in parallel);worker = Bunqueue.Worker.new("emails", fn job -> Bunqueue.Job.update_progress(job, 50, "Sending email...") # Report progress send_email(job.data) # Do the work Bunqueue.Job.log(job, "Email sent successfully") # Attach a log line {:ok, %{sent: true}} # Result, stored end, concurrency: 5) # 5 jobs in parallelReact to events
Section titled “React to events”worker.on('completed', (job, result) => { console.log(`Job ${job.id} completed:`, result);});
worker.on('failed', (job, error) => { console.error(`Job ${job.id} failed:`, error.message);});
worker.on('progress', (job, progress) => { console.log(`Job ${job.id} progress: ${progress}%`);});worker.on('completed', (job, result) => { console.log(`Job ${job.id} completed:`, result);});
worker.on('failed', (job, error) => { console.error(`Job ${job.id} failed:`, error.message);});
worker.on('progress', (job, progress) => { console.log(`Job ${job.id} progress: ${progress}%`);});worker.on("completed", lambda job, result: print(f"Job {job.id} completed: {result}"))worker.on("failed", lambda job, err: print(f"Job {job.id} failed: {err}"))worker.on("progress", lambda job, progress: print(f"Job {job.id} progress: {progress}%"))$worker->on('completed', function ($job, $result) { echo "Job {$job->id()} completed\n";});
$worker->on('failed', function ($job, $err) { echo "Job {$job->id()} failed\n";});
// The PHP worker has no 'progress' event; query progress via the Queue API.worker.On("completed", func(args ...any) { job := args[0].(*bunqueue.Job) fmt.Printf("Job %s completed\n", job.ID())})
worker.On("failed", func(args ...any) { job := args[0].(*bunqueue.Job) fmt.Printf("Job %s failed\n", job.ID())})
// The Go worker has no "progress" event; query progress via the Queue API.// The Rust worker has no event emitter: handle each outcome in the processor,// and use the connection telemetry callback for transport lifecycle events.let worker = Worker::new( "emails", |job| match send_email(job.data()) { Ok(_) => { println!("Job {} completed", job.id()); Ok(Value::from(true)) } Err(e) => { eprintln!("Job {} failed: {e}", job.id()); Err(ProcessError::retryable(e.to_string())) } }, WorkerOptions::default(),);# The Elixir worker has no event emitter: handle each outcome in the handler,# and use the connection `:event_handler` callback for transport lifecycle events.worker = Bunqueue.Worker.new("emails", fn job -> case send_email(job.data) do :ok -> IO.puts("Job #{job.id} completed") {:ok, %{sent: true}}
{:error, reason} -> IO.puts("Job #{job.id} failed: #{inspect(reason)}") {:error, reason} end end)The full event list is in the Worker guide.
Turn on persistence
Section titled “Turn on persistence”Without a data path, jobs live in memory and disappear on restart. Point bunqueue at a SQLite file to survive restarts:
// Option 1: dataPath option (recommended)const queue = new Queue('tasks', { embedded: true, dataPath: './data/bunqueue.db' });const worker = new Worker('tasks', processor, { embedded: true, dataPath: './data/bunqueue.db' });
// Option 2: environment variable// BUNQUEUE_DATA_PATH=./data/bunqueue.db bun run app.tsEvery embedded Queue and Worker in the process shares one database. Naming the same
dataPath again is fine; naming a different one throws instead of silently opening a
second database.
In server mode persistence is configured once on the server, and no client in any language changes:
bunx bunqueue start --data-path ./data/bunq.dbdataPath is an embedded (Bun) option only. BUNQUEUE_DATA_PATH is the canonical variable;
BQ_DATA_PATH, DATA_PATH and SQLITE_PATH are still read, in that order, as fallbacks.
Shut down cleanly
Section titled “Shut down cleanly”import { shutdownManager } from 'bunqueue/client';
process.on('SIGINT', async () => { await worker.close(); // Finish active jobs shutdownManager(); // Flush pending writes, close SQLite process.exit(0);});process.on('SIGINT', async () => { await worker.close(); // Finish active jobs process.exit(0);});worker.close() # Stop pulling, wait for in-flight jobs to drainqueue.close() # Close the connection$worker->installSignalHandlers(); // SIGTERM / SIGINT -> graceful stop$worker->run(); // Returns after the in-flight job finishes$worker->close(); // Unregister and close the connectionworker.Stop() // Stop pulling; in-flight jobs finishworker.Close() // Unregister and close the connectionqueue.Close()worker.stop(); // Stop pulling; in-flight jobs finishworker.close(); // Unregister and close the connectionqueue.close();# Idempotent drain barrier: waits for active handlers, then unregisters and closesBunqueue.Worker.stop(worker)In embedded mode, queue.close() also writes the jobs still in the SQLite write
buffer before it returns, so an awaited add() survives an immediate
process.exit(). It leaves the process-wide timers running, so call
shutdownManager() once every embedded client is closed: that is what lets the
process exit on its own.
Need more than one process?
Section titled “Need more than one process?”The Bun examples above run in a single process. When multiple services need to share one queue, run bunqueue as a standalone server instead:
| Comparison | Embedded mode | Server mode |
|---|---|---|
| Best for | Single-process apps, serverless | Multi-process, microservices |
| Setup | embedded: true | Run bunx bunqueue start, drop the option |
| Clients | Bun only (in process) | Node.js, Deno, Bun, Python, PHP, Go, Rust, Elixir, Cloudflare Workers |
See the Server guide. All six official client SDKs speak the same protocol against the same queues, see the SDK guide. The server uses memory/SQLite by default; for several active brokers sharing one queue, configure the PostgreSQL 15–18 backend; 18.6 is recommended.
Where to go next
Section titled “Where to go next”Less boilerplate. Bunqueue (Simple Mode) wraps Queue + Worker in one object with routes, middleware, and cron:
import { Bunqueue } from 'bunqueue/client';
const app = new Bunqueue('notifications', { embedded: true, routes: { 'send-email': async (job) => ({ sent: true }), 'send-sms': async (job) => ({ sent: true }), }, concurrency: 10,});
await app.add('send-email', { to: 'alice@example.com' });await app.cron('daily-report', '0 9 * * *', { type: 'summary' });import { Bunqueue } from 'bunqueue-client';
const app = new Bunqueue('notifications', { embedded: false, routes: { 'send-email': async (job) => ({ sent: true }), 'send-sms': async (job) => ({ sent: true }), }, concurrency: 10,});
await app.add('send-email', { to: 'alice@example.com' });await app.cron('daily-report', '0 9 * * *', { type: 'summary' });from bunqueue import Bunqueue
app = Bunqueue( "notifications", routes={ "send-email": lambda job: {"sent": True}, "send-sms": lambda job: {"sent": True}, }, concurrency=10,)
app.add("send-email", {"to": "alice@example.com"})app.cron("daily-report", "0 9 * * *", {"type": "summary"})Simple Mode ships in the TypeScript and Python SDKs only. In PHP, compose Queue and
Worker directly, and route on $job->name() inside the processor.
Simple Mode ships in the TypeScript and Python SDKs only. In Go, compose Queue and
Worker directly, and route on job.Name() inside the processor.
Simple Mode ships in the TypeScript and Python SDKs only. In Rust, compose Queue and
Worker directly, and route on job.name() inside the processor.
Simple Mode ships in the TypeScript and Python SDKs only. In Elixir, compose Queue and
Worker directly, and route on job.name inside the handler.
See the Simple Mode guide.
Watch it live. The web dashboard shows queues, jobs, failures, crons, and workers. One command:
bunx bunqueue-dashboardTry the live demo without installing anything, or read the dashboard guide for deployment and security.
Connect AI agents. bunqueue ships an MCP server with 79 tools, so agents like Claude can add jobs, manage crons, and monitor queues via natural language:
bun add -g bunqueue # provides the bunqueue-mcp binarybun add -g @modelcontextprotocol/sdk # required by the MCP server onlyclaude mcp add bunqueue -- bunx --package=bunqueue bunqueue-mcpSetup for Claude Desktop, Cursor, and Windsurf is in the MCP guide.
Orchestrate multi-step processes. The built-in workflow engine (Bun runtime) handles branching, parallel steps, rollback on failure, and human approvals:
import { Workflow, Engine } from 'bunqueue/workflow';
const flow = new Workflow('order') .step('validate', async (ctx) => ({ ok: true })) .step('charge', async (ctx) => ({ txId: 'tx_123' })) .waitFor('manager-approval') // Pauses until you send a signal .step('ship', async (ctx) => ({ shipped: true }));
const engine = new Engine({ embedded: true });engine.register(flow);await engine.start('order', { orderId: 'ORD-1' });See the Workflow Engine guide.
Next steps
Section titled “Next steps”- Queue API, all job options and queue operations
- Worker API, concurrency, events, error handling
- Server Mode, run bunqueue as a standalone server
- Client SDKs, use the queue from Node.js, Deno, Python, PHP, Go, Rust, Elixir
- Code Examples & Recipes, complete examples