- Docs
- Queue
- Progress, Logs & Dependencies
What a job is doing right now.
A job that runs for ten minutes should not be a black box. Progress and logs make it observable, and dependency links show how it relates to the rest of a graph.
Progress, logs, dependencies
Section titled “Progress, logs, dependencies”// Progress and logs (also available on the job object inside a processor)await queue.updateJobProgress('job-id', 75);await queue.addJobLog('job-id', 'Processing step 3 completed');const { logs, count } = await queue.getJobLogs('job-id', 0, 100);
// Parent/child flows (see the Flow guide)const childValues = await queue.getChildrenValues('parent-job-id');const deps = await queue.getJobDependencies('job-id');const processed = await queue.getDependencies('parent-id', 'processed', 0, 10);
// Wait for a job to finish (requires a QueueEvents instance)import { QueueEvents } from 'bunqueue/client';const queueEvents = new QueueEvents('my-queue', { embedded: false, connection: { host: '127.0.0.1', port: 6789 },});await queueEvents.waitUntilReady();const result = await queue.waitJobUntilFinished('job-id', queueEvents, 30000);// Progress and logs (also available on the job object inside a processor)await queue.updateJobProgress('job-id', 75);await queue.addJobLog('job-id', 'Processing step 3 completed');const { logs, count } = await queue.getJobLogs('job-id', 0, 100);
// Parent/child flows (see the Flow guide)const childValues = await queue.getChildrenValues('parent-job-id');const deps = await queue.getJobDependencies('job-id');const processed = await queue.getDependencies('parent-id', 'processed', 0, 10);
// Wait for a job to finish (requires a QueueEvents instance)import { QueueEvents } from 'bunqueue-client';const queueEvents = new QueueEvents('my-queue', { embedded: false, connection: { host: '127.0.0.1', port: 6789 },});await queueEvents.waitUntilReady();const result = await queue.waitJobUntilFinished('job-id', queueEvents, 30000);# Progress and logs (also available on the job object inside a processor)queue.update_job_progress("job-id", 75)queue.add_job_log("job-id", "Processing step 3 completed")logs = queue.get_job_logs("job-id", 0, 100)
# Parent/child flows (see the Flow guide)child_values = queue.get_children_values("parent-job-id")
# Wait for a job to finish (raises on timeout or failure)result = queue.wait_for_job("job-id", timeout_ms=30000)// Progress and logs (progress requires an active job)$queue->getJob('job-id')?->updateProgress(75);$queue->addJobLog('job-id', 'Processing step 3 completed');$logs = $queue->getJobLogs('job-id');
// Parent/child flows (see the Flow guide)$childValues = $queue->getChildrenValues('parent-job-id');
// Wait for a job to finish (throws on timeout or failure)$result = $queue->waitForJob('job-id', 30000);// Progress and logs (job methods; progress requires an active job)job, _ := queue.GetJob("job-id")job.UpdateProgress(75, "")job.Log("Processing step 3 completed", "")logs, _ := queue.GetJobLogs("job-id")
// Parent/child flows (see the Flow guide)childValues, _ := queue.GetChildrenValues("parent-job-id")
// Wait for a job to finish (errors on timeout or failure)result, _ := queue.WaitForJob("job-id", 30000)// Progress and logs (job methods; progress requires an active job)if let Some(job) = queue.get_job("job-id")? { job.update_progress(75.0, None)?; job.log("Processing step 3 completed", None)?;}let logs = queue.get_job_logs("job-id")?;
// Wait for a job to finish (errors on timeout or failure)let result = queue.wait_for_job("job-id", 30_000)?;# Progress and logs (job functions; progress requires an active job){:ok, job} = Bunqueue.Queue.get_job(queue, "job-id"){:ok, _} = Bunqueue.Job.update_progress(job, 75){:ok, _} = Bunqueue.Job.log(job, "Processing step 3 completed"){:ok, logs} = Bunqueue.Queue.get_logs(queue, "job-id")
# Wait for a job to finish (errors on timeout or failure){:ok, result} = Bunqueue.Queue.wait_for_job(queue, "job-id", 30_000)getJobDependencies, getDependencies, and QueueEvents are available in
both TypeScript packages. Bun QueueEvents supports both embedded and TCP brokers;
use the same connection and prefixKey as the Queue. External SDKs expose
waitForJob for the common wait-for-result case and can use SSE/WebSocket for
live queue events.
A wait settles on the job’s final outcome, as in BullMQ: it resolves with the
result once the job completes, even on a later attempt, and rejects with the
last attempt’s error once no retry is left, including a job the stall detector
moved to the dead-letter queue. A failed attempt that will be retried does not
end the wait; on QueueEvents its failed event carries terminal: false.
job.waitUntilFinished(queueEvents, ttl) behaves the same, and also accepts
null instead of QueueEvents, waiting up to ttl (30 seconds by default).
A ttl of 0 means no timeout, with or without QueueEvents.
A job that no longer exists (removed, drained, obliterated) rejects the wait with
Job <id> not found: its outcome is unknown. A job removed on completion
(removeOnComplete) is the exception while the broker still keeps its result
(see below). A wait survives a QueueEvents that closes or reconnects and a broker
outage (rate-limit refusals, timeouts and lost connections are retried, not
reported); only its ttl or a final error such as a refused token ends it early.
After an outage it settles once the client has reconnected, which can lag by the
reconnect backoff (up to 30 seconds) plus the next state read.
Request/reply with removeOnComplete: true works with or without QueueEvents.
The broker keeps the result of a job removed on completion for a short, bounded
time, in memory only: 60 seconds and the last 1,000 such jobs by default
(removedResultRetentionMs / maxRemovedResults, see
Server configuration). A wait that reaches the broker
after the job completed, because a fast worker finished before the first
WaitJob hold arrived or because the wait started after the completed event,
settles on that result instead of rejecting with Job <id> not found. Read the
result within that window: after it, after job.remove(), after the queue is
obliterated, once a new job reuses the same custom ID, or after a broker
restart, the job reads as not found. PostgreSQL keeps its own completion record
with the result. getJobState() still reports unknown and the stored-result
read (GET /jobs/:id/result, TCP GetResult) stays empty for such a job; only
the wait reads the retained result.
QueueEvents remains the best path for high-concurrency request/response: its
completed event delivers the result to every wait at once, without the
per-connection limit on WaitJob holds described below.
Over TCP without QueueEvents, each connection of the pool holds the broker’s
WaitJob for at most 40 waits at a time (160 on the default pool of 4), so the
broker keeps slots for other commands. A wait with a slot sees its job complete
at once; waits beyond that queue for a slot and can see a completion seconds
late (about 2 seconds at p90 with 1,000 concurrent waits on 1-second jobs). A
failure is reported by the next state read: about a second after the wait
starts and at most about 37 seconds late after that, while no more than about
600 waits share the pool (later beyond that). For high-concurrency
request/response, pass a QueueEvents: it reports every completion and failure
as it happens.
Manual state transitions are BullMQ-compatible. Whenever the active job has a
lock, token is mandatory and must be that job’s current worker token in both
embedded and TCP mode. Jobs processed without locks remain administratively
movable without one:
await queue.moveJobToCompleted('job-id', { success: true }, token);await queue.moveJobToFailed('job-id', new Error('reason'), token);await queue.moveJobToWait('job-id', token);await queue.moveJobToDelayed('job-id', Date.now() + 60000, token);await queue.moveJobToWaitingChildren('job-id', token);await queue.moveJobToCompleted('job-id', { success: true }, token);await queue.moveJobToFailed('job-id', new Error('reason'), token);await queue.moveJobToWait('job-id', token);await queue.moveJobToDelayed('job-id', Date.now() + 60000, token);await queue.moveJobToWaitingChildren('job-id', token);queue.move_job_to_completed("job-id", {"success": True}, token)queue.move_job_to_failed("job-id", RuntimeError("reason"), token)queue.move_job_to_wait("job-id", token)queue.move_job_to_delayed("job-id", 60000, token) # delay in ms$queue->moveJobToFailed('job-id', new \RuntimeException('reason'), $token);queue.MoveJobToFailed("job-id", errors.New("reason"), token)// Worker::run() completes or fails pulled jobs with the lease token for you.// Queue-level recovery remains available for an already failed job:queue.retry_job("job-id")?;# This queue helper has no token parameter; use it only for an unlocked job.:ok = Bunqueue.Queue.move_to_delayed(queue, "job-id", 60_000) # delay in ms:ok = Bunqueue.Queue.retry_job(queue, "job-id") # failed -> waitingThe full token-bound transition set is available in both TypeScript packages.
Python supports the transitions except moveJobToWaitingChildren;
PHP and Go expose moveJobToFailed, Elixir exposes move_to_delayed and
retry_job, and Rust intentionally leaves completion/failure acknowledgements
to Worker while exposing queue-level retry.
Where to go next
Section titled “Where to go next”| Guide | What it covers |
|---|---|
| Queue API | Create a queue in embedded or TCP mode |
| Adding Jobs | add, addBulk, priorities, delays, durability |
| Deduplication and Idempotent Job Adds | Idempotent adds, dedup keys, custom job ids |
| Querying Jobs | Fetch jobs, states, counts and results |
| Queue Control and Maintenance | Pause, drain, obliterate, clean and repair |
| Queue Rate Limiting and Global Concurrency | Rate limits and global concurrency caps |
| Job Schedulers from the Queue | Named repeatable schedules from the queue |
| DLQ Operations from the Queue Object | Failed-job operations from the Queue object |
| Workers, Stats and Metrics from the Queue | Registered workers, stats and metrics windows |
| Namespaces, Auto-Batching and Store-and-Forward | Namespaces, auto-batching, store-and-forward |
| JobOptions Reference | Every JobOptions field, with defaults |