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.
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.
Pick an outcome, then step through the job's states.
1 Producerwaiting
2 Ready queuewaiting
3 Workeractive
4 Completedcompleted
1 Producerwaiting
2 Ready queuewaiting
3 Attempt 1active✕ fails
4 Retry delaydelayed
5 Ready againwaiting
6 Attempt 2active
7 Completedcompleted
1 Producerwaiting
2 Ready queuewaiting
3 Attempt 1active✕ fails
4 Retry delaydelayed
5 Final attemptactive✕ fails
6 Dead letter queuefailed
Succeeds first time. Step 1 of 4: Producer, state waiting, and this attempt✕ fails. queue.add() persists the job and returns its ID.
Producerwaitingqueue.add() persists the job and returns its ID.
Ready queuewaiting The job is eligible and waits in scheduling order.
Workeractive One worker claims the job and owns its active attempt.
Completedcompleted The ACK saves the result and releases the concurrency slot.
Producerwaitingqueue.add() persists the job with attempts and backoff.
Ready queuewaiting The first attempt becomes eligible for a worker.
Attempt 1active✕ fails The worker throws, so bunqueue records a failed attempt.
Retry delaydelayed Backoff keeps the job ineligible until its retry time.
Ready againwaiting Once the backoff elapses, the job is eligible again in scheduling order.
Attempt 2active A worker claims the next legal attempt.
Completedcompleted The successful ACK stores the result exactly once.
Producerwaitingqueue.add() persists the job with a finite attempt budget.
Ready queuewaiting The job becomes eligible for its first attempt.
Attempt 1active✕ fails Processing fails and consumes one attempt.
Retry delaydelayed Backoff prevents an immediate hot retry.
Final attemptactive✕ fails The worker fails after the remaining attempt is claimed.
Dead letter queuefailed The terminal failure stays available for inspection or replay.
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.
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.
Adding a job with the jobId of an unfinished job (waiting, delayed, waiting for children, or active) returns the existing job instead of creating a duplicate, so re-runs after a restart do not queue the same work twice. Once that job has completed or moved to the DLQ, the same jobId creates a fresh job, so it does not by itself guarantee “exactly one welcome email per user”.
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.
Pick a topology. The Queue and Worker API stays familiar.
The producer, queue runtime, and worker share one Bun process.
Bun application
ProducerQueue runtimeWorker
direct calls
Optional persistence
SQLite
Best fit
The smallest deployment, local services, and edge processes.
Durability owner
Memory or one local SQLite file.
Independent producers and workers share one bunqueue broker over TCP.
Client processes
ProducerWorker AWorker B
TCP
Queue service
bunqueue broker
local storage
Persistence
SQLite
Best fit
Several processes, several languages, or one central queue service.
Durability owner
The broker owns memory or one SQLite file.
Clients can use any active broker while PostgreSQL coordinates shared queue state.
Client processes
ProducersWorkersQueueEvents
TCP through a load balancer
N active brokers
Broker ABroker BBroker C
transactional coordination
Shared persistence
PostgreSQL
Best fit
Horizontal broker scale, failover, and shared limits across hosts.
Durability owner
PostgreSQL is authoritative for every broker.
Embedded: The producer, queue runtime, and worker share one Bun process.
QueueEvents streaming is available in the Bun bunqueue package and in bunqueue-client (Node.js, Deno); the other SDKs have no QueueEvents stream. See the SDK 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.
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.