Skip to content
Get started
Get started
HTTP REST API Reference: 83 Endpoints + Live Events
View Markdown
api reference · http

The HTTP API, every endpoint.

The bunqueue HTTP API runs on port 6790 by default, configurable via the HTTP_PORT environment variable. All request and response bodies use JSON (Content-Type: application/json) unless otherwise noted.

Response contract: Unless an endpoint explicitly documents another media type or shape, JSON command responses include an ok boolean. Successful responses return "ok": true with operation-specific data; failures return "ok": false with an "error" string. GET /queues/summary is the intentional JSON exception: it returns the summary array directly. Health probe text, Prometheus output, SSE, and WebSocket frames use their documented formats.

// Success
{ "ok": true, "id": "019ce9d7-6983-7000-946f-48737be2b0f9" }
// Error
{ "ok": false, "error": "Job not found" }

When AUTH_TOKENS is configured, protected endpoints require a Bearer token in the Authorization header. Health probes and CORS preflight stay public; /prometheus is also public unless metrics authentication is enabled. Multiple tokens are supported, separated by commas.

# Server configuration (env var)
AUTH_TOKENS=secret-token-1,secret-token-2
# Or in bunqueue.config.ts:
# auth: { tokens: ['secret-token-1', 'secret-token-2'] }
# Client usage
curl -H "Authorization: Bearer secret-token-1" http://localhost:6790/stats

The presented token is compared with each configured token using a constant-time comparison, so a mismatch does not return at the first differing character. The check stops at the first configured token that matches, and each comparison runs over the shorter of the two lengths.

Endpoints that skip authentication:

EndpointReason
GET /healthLoad balancer health checks must work without credentials
GET /healthz, GET /liveKubernetes liveness probes
GET /readyKubernetes readiness probes
GET /prometheusPublic by default; protected when METRICS_AUTH=true
OPTIONS *CORS preflight must respond before auth headers are available

The GET /prometheus endpoint optionally requires auth when requireAuthForMetrics: true is set in the server configuration. This allows Prometheus to scrape without credentials in trusted networks, while requiring auth in public-facing deployments.

Unauthorized response (401):

{ "ok": false, "error": "Unauthorized" }

A 401 carries no Access-Control-Allow-Origin header, even when CORS is configured, so a cross-origin browser client sees a CORS failure rather than this body. The same holds for the 429 rate-limit response.


Cross-Origin Resource Sharing is configured via the CORS_ALLOW_ORIGIN environment variable. By default it is unset, meaning no cross-origin access is granted. Set it explicitly, and only to the origins that need browser access (e.g., CORS_ALLOW_ORIGIN=https://dashboard.example.com). See the Security guide for hardening recommendations.

When CORS is configured, JSON responses from the API routes include the Access-Control-Allow-Origin header (not 401 and 429; see Authentication). Preflight (OPTIONS) requests return:

HTTP/1.1 204 No Content
Access-Control-Allow-Origin: *
Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS
Access-Control-Allow-Headers: Content-Type, Authorization
Access-Control-Max-Age: 86400

The Max-Age: 86400 (24 hours) means browsers cache the preflight response, avoiding repeated OPTIONS requests.


All errors follow a consistent format with appropriate HTTP status codes:

CodeMeaningWhen
200SuccessOperation completed successfully
400Bad RequestInvalid JSON, missing required fields, validation failure (e.g., queue name too long, priority out of range)
401UnauthorizedMissing or invalid Bearer token
404Not FoundUnknown route, or job, cron, webhook, or worker not found
429Rate LimitedClient exceeded the configured request rate
500Internal ErrorUnexpected server error (logged server-side)
503UnavailableStorage degraded (/health, /ready), WS/SSE connection limit reached, or METRICS_AUTH without tokens

Some endpoints, such as DELETE /jobs/:id, queue control, DLQ, rate-limit/concurrency, configuration, and POST /jobs/:id/wait, report command failures with status 200 and "ok": false, so always check ok.

Error response body:

{ "ok": false, "error": "Queue name contains invalid characters" }

Validation rules for the endpoints that admit jobs (single and bulk push; the delay and lock arguments of other job routes are noted where they differ):

  • Queue names: 1-256 characters, alphanumeric + -_.:
  • Numeric fields: Validated by the same validator as the TCP commands, embedded Queue.add and flows, which rejects only what no job can run with: a non-number, NaN, an infinity in any field except maxAttempts (where an infinity means the maximum attempt count), a negative timeout, ttl, backoff or backoff.delay (a missing backoff.delay is the 1000 ms default), and, with groupId, a priority that is not an integer from 0 to 2,097,151. A negative delay makes the job ready at once with a past run time (as in 2.9.10), a move-to-delayed/delay delay is required, and a lock duration must be a finite number. Every value 2.9.10 accepted keeps its 2.9.10 result.
  • Job data: Max 10MB per pushed job payload (updating an existing job’s data has no size limit, as in 2.9.10)
  • Job IDs: UUID v7 format (auto-generated) or custom string (via jobId field)

HTTP requests are rate-limited per client IP using a sliding window algorithm. The client IP is resolved in order: X-Forwarded-For header (first IP) > X-Real-IP header > "unknown".

VariableDefaultDescription
RATE_LIMIT_WINDOW_MS60000Sliding window duration in milliseconds
RATE_LIMIT_MAX_REQUESTS10000Maximum requests per window per IP.
RATE_LIMIT_CLEANUP_MS60000Interval for cleaning up expired rate limit entries

When rate limited, the server responds with:

{ "ok": false, "error": "Rate limit exceeded" }

Status code: 429. The client should implement exponential backoff before retrying.


Understanding the job lifecycle is essential for using the API effectively. A job flows through these states:

Job lifecyclestates and transitions
push priority ≤ 0
→
waiting
push priority > 0
→
prioritized
push delay > 0
→
delayed
→
waiting / prioritized delay expires
↓ pull
active
↓
completed ack
failed fail (terminal)
waiting / prioritized retry
flow dependencies
active
→
waiting-children
→
waiting all children complete

States:

  • waiting, Job is queued with priority ≤ 0
  • prioritized, Job is queued with priority > 0 (processed before waiting jobs)
  • delayed, Job waiting for its delay to expire, then moves to waiting/prioritized
  • active, Job is being processed by a worker
  • completed, Job finished successfully
  • failed, Job failed after all retries (stored in DLQ with attempt history)
  • waiting-children, Parent job waiting for child flow jobs to complete

Delayed jobs: When delay > 0 is set at push time, the job enters delayed state and becomes waiting (or prioritized if priority > 0) after the delay expires. A delayed job can be promoted immediately via the Promote endpoint.

Durable mode: In SQLite mode, durable: true writes the job synchronously before returning. Without it, jobs use the 10ms in-memory write buffer for higher throughput, with a small window of potential data loss on a hard crash. PostgreSQL admissions are transactional regardless of this flag and never use the SQLite buffer.


Add a new job to a queue. The job enters waiting state (or delayed if delay > 0).

POST /queues/:queue/jobs
curl -X POST http://localhost:6790/queues/emails/jobs \
-H "Content-Type: application/json" \
-d '{
"data": {"to": "user@test.com", "subject": "Welcome"},
"priority": 10,
"delay": 5000
}'

Request body, every field is optional:

FieldTypeDefaultDescription
dataany-Job payload. Any JSON-serializable value. Max 10MB. A push without data is accepted and creates a job with no payload.
namestring"default"Job name, stored as the top-level job.name.
prioritynumber0Higher value = processed sooner. Any finite number; with groupId it is the group priority and must be an integer from 0 to 2,097,151.
delaynumber0Milliseconds before the job becomes available for processing. A negative delay makes it ready at once (past run time).
maxAttemptsnumber3Maximum attempts before the job moves to the DLQ. 1 or less runs once, a fraction rounds up, at most 2,147,483,647. attempts is accepted as an alias.
backoffnumber or object1000Base retry delay in milliseconds (exponential: backoff * 2^attempt). Also accepts { "type": "fixed" | "exponential", "delay": ms, "maxDelay": ms } (any other type runs as exponential); the optional maxDelay (0 to 1 day) caps each retry delay (default: 1 hour).
ttlnumber-Time-to-live from creation in milliseconds (0 or more). Job is discarded if not processed within this window.
timeoutnumber-Processing timeout in milliseconds (0 or more; 0 means none). The broker fails the active attempt at its absolute deadline; a later outcome from that lease generation is ignored.
uniqueKeystring-Deduplication key. If a job with the same uniqueKey already exists in the queue, no new job is created: the response is 200 with the existing job’s id.
jobIdstring-Broker-wide custom job ID. If a live job with this ID already exists in any queue, the push is idempotent and returns the existing ID.
tagsstring[][]Metadata tags for filtering and querying.
groupIdstring-Job-group identifier. Claims use ascending group priority with FIFO ties and round-robin across groups; execution is concurrent unless a Worker supplies a group concurrency cap.
lifobooleanfalseLast-in-first-out ordering. When true, the job is processed before other jobs at the same priority.
removeOnCompletebooleanfalseAutomatically remove the job from memory after completion. Saves memory for fire-and-forget jobs.
removeOnFailbooleanfalseAutomatically remove the job after final failure (after all retries exhausted).
durablebooleanfalseSQLite: bypass the write buffer and commit before returning (slower, but no 10 ms buffer-loss window). PostgreSQL admissions are already transactional and do not use the SQLite buffer.
dependsOnstring[][]Job IDs that must complete before this job becomes available. The job enters waiting-children state until all dependencies are met.
repeatobject-Repeat configuration: { every: ms, limit: n } for interval-based, or { pattern: "cron expression" } for cron-based (optional tz, startDate, endDate, immediately).

Any other key in the body is ignored, as it always was: this route forwards only the fields above. To set the other options of the TCP PUSH command (stallTimeout, timestamp, keepLogs, stackTraceLimit, sizeLimit, dedup, debounceId/debounceTtl, groupMaxSize, parentId, the flow failure policies), use POST /queues/:queue/jobs/bulk, which forwards each job object whole.

Success response (200):

{ "ok": true, "id": "019ce9d7-6983-7000-946f-48737be2b0f9" }

The id is a UUID v7 (time-ordered, sortable). If jobId was provided and a live job with that broker-wide ID already exists, including in another queue, the existing job’s ID is returned (idempotent).

Error responses:

StatusErrorCause
400Invalid JSON bodyRequest body is not valid JSON
404Not foundEmpty queue name (no route matches)
400Queue name contains invalid charactersQueue name has chars outside a-zA-Z0-9_-.:
400Job data too large (max 10MB)Serialized data exceeds 10MB
400priority must be a numberNon-numeric priority ("high")
400delay must be a numberNon-numeric delay (a negative one runs at once)
400timeout must be at least 0Negative processing timeout
400backoff.delay must be at least 0Negative object-form backoff.delay (a missing one is 1000)

Push multiple jobs to a queue in a single round-trip. More efficient than individual pushes, all jobs are inserted in a single batch operation.

POST /queues/:queue/jobs/bulk
curl -X POST http://localhost:6790/queues/emails/jobs/bulk \
-H "Content-Type: application/json" \
-d '{
"jobs": [
{"data": {"to": "user1@test.com"}, "priority": 5},
{"data": {"to": "user2@test.com"}},
{"data": {"to": "user3@test.com"}, "delay": 60000}
]
}'

Each item in jobs supports the same fields as a single push and is validated with the same rules (option bounds and dependsOn existence; a dependsOn entry may also reference the customId of any job in the same batch, in any order). Two differences: the custom ID field is customId (not jobId), and the attempts alias is not accepted (use maxAttempts). Validation is all-or-nothing: if any job fails validation, the whole batch is rejected with an error naming the offending index (jobs[i]: ...) and nothing is pushed. A runtime admission error after validation, such as a full group (group.maxSize) or an unresolved custom-ID dependency, can leave the jobs accepted before it pushed, so check which IDs exist before you resubmit.

Response (200):

{ "ok": true, "ids": ["id-1", "id-2", "id-3"] }

IDs are returned in the same order as the input jobs.


Pull the next available job from a queue for processing. The job transitions from waiting to active state. Respects priority ordering (higher priority first) and FIFO within the same priority.

GET /queues/:queue/jobs[?timeout=ms]
# Immediate return (no wait), returns null if queue is empty
curl http://localhost:6790/queues/emails/jobs
# Long-poll for up to 5 seconds, waits for a job to become available
curl http://localhost:6790/queues/emails/jobs?timeout=5000
ParameterTypeDefaultMaxDescription
timeoutnumber060000Long-poll timeout in ms. 0 = return immediately if no job available.

timeout is read with parseInt, as in every release (5000ms is 5000, 1e3 is 1). The route always answers 200; an invalid wait is reported in the body with "ok": false: timeout must be at most 60000, timeout must be at least 0, or timeout must be a finite number for an empty or non-numeric value.

Response with job (200):

{
"ok": true,
"job": {
"id": "019ce9d7-6983-7000-946f-48737be2b0f9",
"queue": "emails",
"data": { "to": "user@test.com", "subject": "Welcome" },
"priority": 10,
"createdAt": 1700000000000,
"runAt": 1700000000000,
"attempts": 0,
"maxAttempts": 3,
"backoff": 1000,
"progress": 0,
"tags": [],
"lifo": false,
"removeOnComplete": false,
"removeOnFail": false
}
}

No job available (200):

{ "ok": true, "job": null }

Behavior notes:

  • Paused queues return null even if jobs exist
  • HTTP is stateless, so a pulled job is not tied to the client connection. If the client never ACKs or fails it, the stall detector eventually recovers the job (retry, or DLQ after maxStalls)
  • Rate-limited queues may return null even if jobs exist (rate limit exceeded)
  • Per-group concurrency: if the job’s groupId has reached its concurrency limit, the next job from a different group is returned

Pull multiple jobs at once. More efficient than individual pulls for high-throughput workers.

POST /queues/:queue/jobs/pull-batch
curl -X POST http://localhost:6790/queues/emails/jobs/pull-batch \
-H "Content-Type: application/json" \
-d '{"count": 10, "timeout": 5000}'
FieldTypeRequiredRangeDescription
countnumberYes1-1000Number of jobs to pull
timeoutnumberNo0-60000Long-poll timeout (ms), honored with or without owner. Default 0 (return immediately).
ownerstringNo-Lock owner identifier for lock-based processing
lockTtlnumberNo-Lock time-to-live (ms). Job is released if lock expires without ACK.

Response (200):

{
"ok": true,
"jobs": [
{"id": "id-1", "queue": "emails", "data": {...}, "priority": 5, ...},
{"id": "id-2", "queue": "emails", "data": {...}, "priority": 3, ...}
]
}

Returns fewer jobs than count if the queue doesn’t have enough available jobs.

With owner, the response also carries tokens: the lock token of each job, in the same order as jobs. Send each token back with its job’s ack, fail and heartbeat requests.


Retrieve a job by ID. Returns the full job object regardless of state (waiting, active, delayed, completed).

GET /jobs/:id
curl http://localhost:6790/jobs/019ce9d7-6983-7000-946f-48737be2b0f9

Response (200):

{
"ok": true,
"job": {
"id": "019ce9d7-6983-7000-946f-48737be2b0f9",
"queue": "emails",
"data": { "to": "user@test.com" },
"priority": 0,
"createdAt": 1700000000000,
"runAt": 1700000000000,
"startedAt": 1700000001000,
"completedAt": null,
"attempts": 1,
"maxAttempts": 3,
"backoff": 1000,
"progress": 50,
"tags": ["onboarding"],
"lifo": false,
"removeOnComplete": false,
"removeOnFail": false
}
}

Not found (404): { "ok": false, "error": "Job not found" }


Look up a job using the custom jobId that was set at push time. Useful for idempotent workflows where you generate your own IDs.

GET /jobs/custom/:customId
curl http://localhost:6790/jobs/custom/order-12345

Returns the same response format as GET /jobs/:id.


GET /jobs/:id/state
{ "ok": true, "id": "019ce9d7-...", "state": "active" }

Possible states: waiting, prioritized, delayed, active, waiting-children, completed, failed, unknown (job not found)


Retrieve the result stored when a job was acknowledged. Only available for completed jobs.

GET /jobs/:id/result
{ "ok": true, "id": "019ce9d7-...", "result": { "sent": true, "messageId": "abc-123" } }

Results are stored in an LRU cache (max 10,000 entries by default). Oldest results are evicted when the cache is full. For permanent result storage, use the result field in your own database.


Remove a job. Works on waiting, prioritized, delayed, and waiting-children jobs, and on completed jobs: their data and result are deleted (a payload-free completion record stays, so jobs that depend on it still run, exactly as with removeOnComplete). active and failed jobs cannot be cancelled; use the DLQ endpoints for failed jobs.

DELETE /jobs/:id
curl -X DELETE http://localhost:6790/jobs/019ce9d7-...

Response (200): { "ok": true }

When nothing was removed, the response is still 200, with ok: false:

  • the job does not exist: { "ok": false, "error": "Job not found or cannot be cancelled" };
  • the job exists but cannot be removed: { "ok": false, "error": "Job cannot be cancelled: not in queue (current state: active)" } (or failed; on PostgreSQL also a waiting job that another live job depends on, with current state: waiting).

Removing a job deletes its data and result, not the events already recorded about it: the queue’s bounded event journal (SQLite queue_events, PostgreSQL bunqueue_events) keeps them, including the completed event with the return value, until its retention trims them, exactly as for removeOnComplete.


Mark a job as successfully completed. The job transitions from active to completed state. Optionally store a result that can be retrieved later via GET /jobs/:id/result.

POST /jobs/:id/ack
curl -X POST http://localhost:6790/jobs/019ce9d7-.../ack \
-H "Content-Type: application/json" \
-d '{"result": {"sent": true, "messageId": "abc-123"}}'

Request body (optional):

FieldTypeDescription
resultanyCompletion result. Stored in LRU cache (10,000 max).
tokenstringLock token (if using lock-based processing).

Response (200): { "ok": true }

Error (400): { "ok": false, "error": "Job not found or not in processing state: <id>" }

What happens on ACK:

  1. Job is removed from the active processing queue
  2. Result is stored in the LRU cache (if provided)
  3. Completion counter incremented
  4. job:completed event broadcast to all subscribers
  5. queue:counts event broadcast with updated counts
  6. Dependent jobs (via dependsOn) are checked and promoted if all dependencies are met
  7. If removeOnComplete: true, the job is permanently deleted from memory

Acknowledge multiple jobs in a single round-trip.

POST /jobs/ack-batch
curl -X POST http://localhost:6790/jobs/ack-batch \
-H "Content-Type: application/json" \
-d '{"ids": ["id-1", "id-2", "id-3"], "results": [{"a": 1}, null, {"c": 3}]}'
FieldTypeRequiredDescription
idsstring[]YesJob IDs to acknowledge
resultsunknown[]NoPer-job results (positional, same order as ids)
tokensstring[]NoLock tokens (positional)

Mark a job as failed. If retry attempts remain, the job is automatically re-queued with exponential backoff (backoff * 2^attempt). If all attempts are exhausted, the job moves to the Dead Letter Queue (DLQ).

POST /jobs/:id/fail
curl -X POST http://localhost:6790/jobs/019ce9d7-.../fail \
-H "Content-Type: application/json" \
-d '{"error": "SMTP connection refused"}'
FieldTypeDescription
errorstringError message. Stored with the job for debugging.
tokenstringLock token (if using lock-based processing).
unrecoverablebooleanSkip remaining retries and fail terminally (straight to DLQ).
stackstring[]Stack trace lines, stored on the job and its DLQ entry.

Retry behavior:

Retry behaviorexponential backoff
Attempt 1 fails
→
wait ~2s backoff * 2
→
retry
Attempt 2 fails
→
wait ~4s backoff * 4
→
retry
Attempt 3 fails
→
move to DLQ maxAttempts reached

With a numeric backoff (default 1000, maxAttempts 3), the retry delay is backoff * 2^attempt, where attempt is the number of failed attempts so far, multiplied by a random jitter factor between 0.5 and 1.5 and capped at 1 hour. With { "type": "fixed", "delay": ms } the delay is delay with ±20% jitter. The object form also accepts maxDelay (0 to 86,400,000 ms), which replaces the 1-hour cap for that job: { "type": "exponential", "delay": 1000, "maxDelay": 30000 } never waits more than 30 seconds between attempts. An invalid maxDelay is rejected with 400.


Edit the JSON payload of a job in-place. Works on jobs that are waiting, prioritized, delayed, paused, waiting-children (or waiting on dependsOn) or active. On a completed repeatable job it updates the next repetition, if that one is still queued. Useful for modifying job parameters before processing or while a job is being retried.

PUT /jobs/:id/data
curl -X PUT http://localhost:6790/jobs/019ce9d7-.../data \
-H "Content-Type: application/json" \
-d '{"data": {"to": "new@email.com", "subject": "Updated subject"}}'

The entire data field is replaced (not merged). To update a single field, read the current data first, modify it, then PUT the full object.

Broadcasts: job:data-updated event.


Change the priority of a job in waiting or delayed state. Higher priority = processed sooner.

PUT /jobs/:id/priority
{ "priority": 100, "lifo": true }

lifo is optional; it sets the tie-break order among jobs of the same priority, as on the TCP ChangePriority command. The job is repositioned in the priority queue immediately. Does not work on active jobs (they’re already being processed).

Broadcasts: job:priority-changed event with { jobId, newPriority }.


Move a job from delayed to waiting state for immediate processing. The job becomes available for the next PULL operation.

POST /jobs/:id/promote
curl -X POST http://localhost:6790/jobs/019ce9d7-.../promote

Error (400): { "ok": false, "error": "Job not found or not delayed" }, returned if the job doesn’t exist, is already in waiting state, or is active.

Broadcasts: job:promoted event.


Move a job back to waiting, dispatching on its current state: an active job is released back to the queue, a delayed job is promoted, a failed job is retried from the DLQ, and a waiting/prioritized job is a successful no-op. Other states return an error.

POST /jobs/:id/move-to-wait
{ "token": "lock-token" }

token is optional; it is required only for an active job that holds a lock.


Move an active job back to delayed state. Useful when a worker determines it can’t process the job right now but doesn’t want to fail it.

POST /jobs/:id/move-to-delayed
{ "delay": 60000, "token": "lock-token" }

token is optional; it is required only for an active job that holds a lock. The job will become waiting again after delay milliseconds.


Update the delay of a job. For a job still in the queue (waiting, prioritized, delayed or paused) the runAt time is recalculated as now + delay; an active job moves to delayed, as with Move to Delayed. This route forwards no lock token, so an active job that holds a lock is refused; use Move to Delayed with its token.

PUT /jobs/:id/delay
{ "delay": 30000 }

Broadcasts: job:delay-changed event with { jobId, newDelay }.


Move a job directly to the Dead Letter Queue, bypassing the normal retry mechanism. Works on waiting, delayed, and active jobs.

POST /jobs/:id/discard
curl -X POST http://localhost:6790/jobs/019ce9d7-.../discard

Broadcasts: job:discarded event.


Long-poll until a job completes or the timeout expires. This is event-driven (not polling), the server subscribes to the job’s completion event internally and resolves immediately when the job finishes.

POST /jobs/:id/wait
curl -X POST http://localhost:6790/jobs/019ce9d7-.../wait \
-H "Content-Type: application/json" \
-d '{"timeout": 30000}'
FieldTypeDefaultDescription
timeoutnumber30000Maximum wait time in milliseconds (max: 600000)

Completed within timeout:

{ "ok": true, "completed": true, "result": { "sent": true } }

Timed out:

{ "ok": true, "completed": false }

Not found:

{ "ok": false, "error": "Job not found" }

If the job is already completed when the request arrives, the result is returned immediately without waiting.


Workers can report progress (0-100) during long-running jobs. The dashboard can display this as a progress bar.

Get current progress:

GET /jobs/:id/progress
{ "ok": true, "progress": 75, "message": "Processing attachments..." }

Update progress:

POST /jobs/:id/progress
{ "progress": 75, "message": "Processing attachments..." }

Progress is stored on the job object and broadcast as a job:progress event to all WebSocket subscribers.


For a flow parent job (one created with children, for example by FlowProducer), retrieve the stored results of its completed children. Keys are <queue>:<childId>, or the bare childId when the child job no longer exists.

GET /jobs/:id/children
{
"ok": true,
"data": { "values": { "emails:child-job-1": { "sent": true }, "emails:child-job-2": { "sent": true } } }
}

Send a heartbeat to prevent the stall detector from marking the job as stalled. Workers should send heartbeats at regular intervals (default: every 10 seconds) for long-running jobs.

POST /jobs/:id/heartbeat
{ "token": "lock-token", "duration": 30000 }

Both fields are optional. If the job doesn’t exist or isn’t active, returns an error.

Batch heartbeat:

POST /jobs/heartbeat-batch
{ "ids": ["id-1", "id-2"], "tokens": ["tok-1", "tok-2"] }

Extend the lock TTL on an active job. Used in lock-based processing where a worker holds a lock on a job and needs more time.

POST /jobs/:id/extend-lock
{ "duration": 30000, "token": "lock-token" }

Batch extend:

POST /jobs/extend-locks
{ "ids": ["id-1", "id-2"], "tokens": ["tok-1", "tok-2"], "durations": [30000, 60000] }

Structured logging attached to individual jobs. Useful for debugging failed jobs, each log entry has a level and message.

Add a log entry:

POST /jobs/:id/logs
curl -X POST http://localhost:6790/jobs/019ce9d7-.../logs \
-H "Content-Type: application/json" \
-d '{"message": "Connecting to SMTP server...", "level": "info"}'
FieldTypeRequiredDescription
messagestringYesLog message
levelstringNoinfo (default), warn, or error

Logs are stored in an LRU cache (max 100 entries per job, 10,000 jobs total).

Get all logs:

GET /jobs/:id/logs

Clear logs:

DELETE /jobs/:id/logs

Returns all queue names that have had at least one job pushed to them. Queue names persist until the queue is obliterated.

GET /queues
{ "ok": true, "queues": ["emails", "notifications", "reports"] }

All queues with paused state and per-state counts in a single call (one round-trip instead of N).

GET /queues/summary
[
{
"name": "emails",
"paused": false,
"counts": {
"waiting": 125,
"prioritized": 7,
"active": 5,
"completed": 10234,
"failed": 23,
"delayed": 2
}
}
]

Note: this endpoint returns a bare JSON array (no ok wrapper).


Workers currently registered for a specific queue.

GET /queues/:queue/workers
{
"ok": true,
"workers": [
{
"id": "w-1",
"name": "email-worker",
"queues": ["emails"],
"concurrency": 5,
"registeredAt": 1700000000000,
"lastSeen": 1700000010000,
"activeJobs": 3,
"processedJobs": 1500,
"failedJobs": 12
}
]
}

Paginated listing of jobs in a specific queue, filtered by state.

GET /queues/:queue/jobs/list[?status=waiting&limit=10&offset=0]
curl "http://localhost:6790/queues/emails/jobs/list?status=waiting&limit=20&offset=0"
# multiple states (comma-separated or repeated):
curl "http://localhost:6790/queues/emails/jobs/list?status=failed,completed"
ParameterTypeDefaultDescription
statusstringallState filter: waiting, prioritized, delayed, active, completed, failed, waiting-children. Aliases: state, states. Repeatable and comma-separated for multiple states.
limitnumber100Max jobs to return
offsetnumber0Skip first N jobs

limit and offset are read with parseInt, as in every release (limit=1e2 is 1).

Response (200):

{
"ok": true,
"jobs": [
{"id": "...", "queue": "emails", "data": {...}, "priority": 5, "createdAt": 1700000000000, "runAt": 1700000000000, "attempts": 0, "progress": 0}
]
}

Jobs are ordered by createdAt ascending (oldest first), with the job ID as a deterministic tie-breaker. State filtering happens before offset and limit, so pages are stable even when createdAt values match.


Returns the number of jobs in each state for a specific queue.

GET /queues/:queue/counts
{
"ok": true,
"counts": {
"waiting": 150,
"prioritized": 0,
"active": 12,
"delayed": 30,
"completed": 5000,
"failed": 3,
"waiting-children": 4,
"paused": 0
}
}

Returns the number of queued jobs (waiting, prioritized, and delayed) in a queue. Active, completed, and failed jobs are not counted.

GET /queues/:queue/count
{ "ok": true, "count": 180 }

Returns a breakdown of jobs by priority level. Useful for dashboards showing priority distribution.

GET /queues/:queue/priority-counts
{ "ok": true, "queue": "emails", "counts": { "0": 100, "5": 30, "10": 12 } }

GET /queues/:queue/paused
{ "ok": true, "paused": false }

Stop processing new jobs from this queue. Active jobs continue to completion, only new pulls are blocked.

POST /queues/:queue/pause

Broadcasts: queue:paused event.


Resume processing after a pause.

POST /queues/:queue/resume

Broadcasts: queue:resumed event.


Remove all waiting and delayed jobs from a queue. Active jobs are not affected, they continue processing normally. This is useful for clearing a backlog without affecting in-progress work.

POST /queues/:queue/drain
{ "ok": true, "count": 150 }

Broadcasts: queue:drained event with { queue, count }.


Completely destroy a queue and all its jobs (waiting, delayed, and metadata). Active jobs continue but their ACK/FAIL will be no-ops.

POST /queues/:queue/obliterate

Broadcasts: queue:obliterated event.


Remove jobs older than a grace period, optionally filtered by state. Useful for maintenance, cleaning up old waiting/delayed jobs that are no longer relevant.

POST /queues/:queue/clean
curl -X POST http://localhost:6790/queues/emails/clean \
-H "Content-Type: application/json" \
-d '{"grace": 86400000, "state": "waiting", "limit": 500}'
FieldTypeDefaultDescription
gracenumber0Only remove jobs older than this many milliseconds. 0 = remove all.
statestringqueuedwaiting/delayed/prioritized/paused (all clean the queued set, the default), completed, or failed.
limitnumber1000Max jobs to remove per call.

Response (200):

{ "ok": true, "count": 42, "ids": ["019ce9d7-...", "..."] }

Uses a temporal index for efficient O(log n + k) cleanup instead of full queue scan.

Broadcasts: queue:cleaned event with { queue, state, count }.


Move all (or up to N) delayed jobs in a queue to waiting state immediately.

POST /queues/:queue/promote-jobs
{ "count": 50 }

Omit count to promote all delayed jobs.


Re-queue completed jobs for reprocessing. Useful for replaying jobs after a bug fix.

POST /queues/:queue/retry-completed
{ "id": "specific-job-id" }

Omit id to retry all completed jobs in the queue.


Jobs that exhaust all retry attempts or are explicitly discarded land in the DLQ. Each queue has its own DLQ. DLQ entries include the original job data, failure reason, and timestamp.

GET /queues/:queue/dlq[?limit=100&offset=0]
ParameterTypeDefaultDescription
limitnumberallMax entries to return
offsetnumber0Skip first N entries

Returns full DLQ entries (original job + failure metadata) under entries, plus the total count for pagination:

{
"ok": true,
"entries": [
{
"job": { "id": "...", "data": {}, "attempts": 3 },
"enteredAt": 1700000000000,
"reason": "max_attempts_exceeded",
"error": "SMTP timeout",
"attempts": [
{
"attempt": 1,
"startedAt": 1700000000000,
"failedAt": 1700000001000,
"reason": "explicit_fail",
"error": "SMTP timeout",
"duration": 1000
}
],
"retryCount": 0
}
],
"total": 1
}

Omit limit/offset to return all entries.


Aggregated DLQ statistics for a queue.

GET /queues/:queue/dlq/stats
{
"ok": true,
"stats": {
"total": 12,
"byReason": {
"explicit_fail": 4,
"max_attempts_exceeded": 6,
"timeout": 1,
"stalled": 1,
"ttl_expired": 0,
"worker_lost": 0,
"unknown": 0
},
"byQueue": { "emails": 12 },
"pendingRetry": 0,
"expired": 0,
"oldestEntry": 1700000000000,
"newestEntry": 1700003600000
}
}

Re-queue jobs from the DLQ back to the main queue for reprocessing. The job’s attempt counter is reset.

POST /queues/:queue/dlq/retry
{ "jobId": "specific-job-id" }

Omit jobId to retry all DLQ jobs. Returns { "ok": true, "count": 5 }.

Broadcasts: dlq:retried (single) or dlq:retry-all (all) event.


Remove all jobs from the DLQ permanently. This is irreversible.

POST /queues/:queue/dlq/purge
{ "ok": true, "count": 12 }

Broadcasts: dlq:purged event with { queue, count }.


Per-queue controls for throughput and parallelism. These are queue-level settings, independent of HTTP rate limiting.

Limit the number of jobs that can be processed from a queue: limit jobs per duration ms (default 1000, so jobs per second).

PUT /queues/:queue/rate-limit
{ "limit": 100, "duration": 60000, "ttl": 30000 }

duration and ttl are optional. duration sets the window in ms; ttl makes the limit temporary, the server clears it by itself after that many ms. Invalid values fall back to the defaults (1 second window, permanent limit).

When the rate limit is hit, workers pulling from this queue receive null until the next window opens.

Broadcasts: ratelimit:set event.

DELETE /queues/:queue/rate-limit

Broadcasts: ratelimit:cleared event.

Limit the number of jobs that can be processed simultaneously from a queue.

PUT /queues/:queue/concurrency
{ "concurrency": 5 }

Accepts either concurrency (natural for this endpoint) or limit. A non-numeric value is rejected.

Broadcasts: concurrency:set event.

DELETE /queues/:queue/concurrency

Broadcasts: concurrency:cleared event.


Stall detection identifies jobs that a worker started processing but never acknowledged. This can happen when a worker crashes, hangs, or loses network connectivity.

Get current config:

GET /queues/:queue/stall-config

Update config:

PUT /queues/:queue/stall-config
{
"config": {
"stallInterval": 30000,
"maxStalls": 3,
"gracePeriod": 5000
}
}
FieldDefaultDescription
enabledtrueTurn stall detection on or off for this queue
stallInterval30000A job is stalled after this many ms without a heartbeat
maxStalls3Max times a job can stall before moving to DLQ
gracePeriod5000Grace period after job starts before stall detection kicks in

Broadcasts: config:stall-changed event.

Get current config:

GET /queues/:queue/dlq-config

Update config:

PUT /queues/:queue/dlq-config
{
"config": {
"autoRetry": true,
"maxAge": 604800000,
"maxEntries": 10000
}
}
FieldDefaultDescription
autoRetryfalseAutomatically retry DLQ entries after a delay
autoRetryInterval3600000Delay between automatic retries of an entry, in ms (default: 1 hour)
maxAutoRetries3Automatic retries of an entry before auto-retry gives up on it
maxAge604800000Max age of DLQ entries in ms (default: 7 days; null = never). Older entries are removed.
maxEntries10000Max DLQ entries per queue. Oldest are evicted when full.

Broadcasts: config:dlq-changed event.


Schedule recurring jobs using cron expressions or fixed intervals.

GET /crons
{
"ok": true,
"crons": [
{
"name": "daily-cleanup",
"queue": "maintenance",
"schedule": "0 2 * * *",
"repeatEvery": null,
"nextRun": 1700100000000,
"executions": 42,
"maxLimit": null,
"timezone": "UTC"
}
]
}

POST /crons
curl -X POST http://localhost:6790/crons \
-H "Content-Type: application/json" \
-d '{
"name": "daily-cleanup",
"queue": "maintenance",
"data": {"task": "cleanup-stale-sessions"},
"schedule": "0 2 * * *",
"timezone": "America/New_York"
}'
FieldTypeRequiredDescription
namestringYesUnique identifier. Re-using a name updates the existing cron.
jobNamestringNoName assigned to generated jobs (default "default").
queuestringYesTarget queue for the generated jobs.
dataanyYesJob payload pushed on each execution.
schedulestring*Cron expression ("*/5 * * * *", "0 2 * * *").
repeatEverynumber*Positive safe-integer interval in ms (alternative to cron expression).
timezonestringNoIANA timezone. Raw HTTP uses the server/system timezone when omitted.
prioritynumberNoPriority for generated jobs.
maxLimitnumberNoMax total executions. Cron is removed after reaching this count.
immediatelybooleanNoFire once on creation, then continue on schedule (default false).
skipIfNoWorkerbooleanNoSkip a tick when no worker is registered for the queue (default false).
preventOverlapbooleanNoDeduplicate overlapping runs, a tick is skipped while the previous generated job is still pending/active (default true).
uniqueKeystringNoDeduplication key for the generated jobs.
dedupobjectNoDeduplication options for the generated jobs: { ttl?, extend?, replace? } (ttl finite).
skipMissedOnRestartbooleanNoSkip ticks missed while the server was down instead of running them on restart (default true).
jobOptionsobjectNoPer-job options applied to every generated job: maxAttempts, backoff, timeout, delay, stallTimeout, removeOnComplete, removeOnFail.

* At least one of schedule or repeatEvery is required. When both are valid, schedule takes precedence for backward compatibility.

Broadcasts: cron:created event (cron:updated when the name already existed).


GET /crons/:name

DELETE /crons/:name

Broadcasts: cron:deleted event.


Register HTTP endpoints to be called when specific job events occur. A failed delivery is retried with linear backoff: up to 3 attempts in total, first try included (WEBHOOK_MAX_RETRIES), waiting 1s, then 2s (WEBHOOK_RETRY_DELAY_MS times the attempt number).

GET /webhooks

POST /webhooks
curl -X POST http://localhost:6790/webhooks \
-H "Content-Type: application/json" \
-d '{
"url": "https://example.com/hooks/bunqueue",
"events": ["job.completed", "job.failed"],
"queue": "emails",
"secret": "whsec_abc123"
}'
FieldTypeRequiredDescription
urlstringYesHTTP or HTTPS endpoint URL. Validated against SSRF (localhost, private IPs, cloud metadata blocked).
eventsstring[]YesEvent types: job.pushed, job.started, job.completed, job.failed, job.progress. Other values are rejected.
queuestringNoFilter to specific queue. Omit for all queues.
secretstringNoHMAC signing secret for verifying webhook authenticity.

Response (200):

{
"ok": true,
"data": {
"webhookId": "019ce9d7-7a10-7000-8000-4f2a9b1c3d5e",
"url": "https://...",
"events": ["job.completed", "job.failed"],
"queue": "emails",
"createdAt": 1700000000000
}
}

Broadcasts: webhook:added event.


DELETE /webhooks/:id

Broadcasts: webhook:removed event.


PUT /webhooks/:id/enabled
{ "enabled": false }

Disabled webhooks stop receiving deliveries but retain their configuration.


GET /workers
{
"ok": true,
"data": {
"workers": [
{
"id": "w-1",
"name": "email-worker",
"queues": ["emails"],
"lastSeen": 1700000000000,
"activeJobs": 3,
"processedJobs": 1500,
"failedJobs": 12
}
],
"stats": { "total": 4, "active": 3 }
}
}

POST /workers
{ "name": "email-worker-1", "queues": ["emails", "notifications"] }

Optional fields, as on the TCP RegisterWorker command: concurrency (number), workerId (reuse a stable worker ID across re-registrations), hostname, pid and startedAt (ms timestamp).

Broadcasts: worker:connected event with { workerId, name, queues }.


DELETE /workers/:id

Broadcasts: worker:disconnected event with { workerId }.


Keep a worker’s registration alive. Workers that stop sending heartbeats are eventually marked as disconnected.

POST /workers/:id/heartbeat

Comprehensive health information for load balancers and monitoring systems. No authentication required.

GET /health
{
"ok": true,
"status": "healthy",
"uptime": 86400,
"version": "x.y.z",
"queues": { "waiting": 150, "active": 12, "delayed": 30, "completed": 50000, "dlq": 3 },
"connections": { "tcp": 8, "ws": 4, "sse": 2 },
"memory": { "heapUsed": 45, "heapTotal": 64, "rss": 82 }
}

Memory values in MB. Uptime in seconds. When storage is degraded (for example, disk full), it returns HTTP 503 with "ok": false, "status": "degraded", and a storage object (diskFull, error, since).


GET /healthz # Returns "OK" (text/plain, 200)
GET /live # Returns "OK" (text/plain, 200)
GET /ready # Returns { "ok": true, "ready": true }

No authentication required. Designed for Kubernetes probe configuration.


GET /ping
{ "ok": true, "data": { "pong": true, "time": 1700000000000 } }

Server statistics with throughput counters, memory usage, and internal collection sizes.

GET /stats
{
"ok": true,
"stats": {
"waiting": 150,
"active": 12,
"delayed": 30,
"completed": 50000,
"dlq": 3,
"totalPushed": 100000,
"totalPulled": 99500,
"totalCompleted": 98000,
"totalFailed": 200,
"uptime": 86400
},
"memory": { "heapUsed": 45, "heapTotal": 64, "rss": 82, "external": 2, "arrayBuffers": 1 },
"collections": {
"jobIndex": 1500,
"completedJobs": 5000,
"processingTotal": 12,
"queuedTotal": 150,
"temporalIndexTotal": 30
}
}

GET /metrics
{
"ok": true,
"metrics": {
"totalPushed": 100000,
"totalPulled": 99500,
"totalCompleted": 98000,
"totalFailed": 200
}
}

GET /prometheus

Returns text/plain; version=0.0.4 format for Prometheus scraping. Includes per-queue gauges, throughput counters, and latency histograms. Optionally requires auth (requireAuthForMetrics).


GET /storage
{ "ok": true, "data": { "diskFull": false, "error": null, "since": null } }

In SQLite mode, diskFull: true means durable writes are rejected; existing in-memory work can continue while health remains degraded. In PostgreSQL mode, diskFull is not a local-disk signal: error and since report database or projection degradation, affected queue operations can reject, and /ready returns 503 until authority is restored. Memory-only mode has no persistent storage health to report.


POST /gc

Triggers Bun GC and internal memory compaction (compactMemory()). Returns before/after heap stats in MB.

{
"ok": true,
"before": { "heapUsed": 52, "heapTotal": 64, "rss": 90 },
"after": { "heapUsed": 45, "heapTotal": 64, "rss": 85 }
}

GET /heapstats

Detailed V8/JSC heap breakdown for debugging memory leaks. Returns top 20 object types by count, internal collection sizes, and heap metrics.


Aggregated read-only snapshots designed for dashboards (fewer round-trips than composing the individual endpoints).

GET /dashboard

Single call returning stats (global counts + totals + uptime), throughput (per-second rates), latency (averages is flat, { pushMs, pullMs, ackMs }; percentiles is nested per operation, push/pull/ack each with p50, p95, p99), memory, collections, workers (stats + list, capped at 100 with a truncated flag), crons (total + list, capped at 100 with a truncated flag), storage, and timestamp.

GET /dashboard/queues[?limit=100&offset=0]
ParameterTypeDefaultDescription
limitnumber100Max queues to return (1-500)
offsetnumber0Skip first N queues
{
"ok": true,
"queues": [
{ "name": "emails", "waiting": 125, "delayed": 2, "active": 5, "dlq": 3, "paused": false }
],
"total": 3,
"limit": 100,
"offset": 0,
"timestamp": 1700000000000
}
GET /dashboard/queues/:queue[?includeJobs=true]

Returns counts (all 8 states, paused-aware, plus the queue’s lifetime totalCompleted and totalFailed), paused, priorityCounts, a dlqPreview (up to 10 entries), and, with includeJobs=true, up to 10 job summaries per state (waiting, active, delayed, paused).


bunqueue provides two real-time event channels: Server-Sent Events (SSE) for simple one-way streaming, and WebSocket with full pub/sub for interactive dashboards.

GET /events
GET /events/queues/:queue

SSE sends typed events. Each job event has an id: (used for Last-Event-ID resume), an event: name identical to the WebSocket pub/sub names (job:completed, job:failed, …), and a JSON data: payload { queue, jobId, timestamp, error?, progress?, prev?, delay? }. Dashboard and periodic events (queue:counts, queue:paused, stats:snapshot every 5s, health:status every 10s, storage:status every 30s, …) use the same stream with their pub/sub payloads; /events/queues/:queue filters only job events. The first message is an unnamed { "connected": true, "clientId": "..." }, and a :heartbeat comment is sent every 30 seconds. Because events are named, listen with addEventListener; onmessage only receives the unnamed connection message. For authenticated SSE, use @microsoft/fetch-event-source (native EventSource doesn’t support custom headers).

const events = new EventSource('http://localhost:6790/events');
for (const name of ['job:completed', 'job:failed']) {
events.addEventListener(name, (e) => {
const data = JSON.parse(e.data);
console.log(`[${e.type}] ${data.queue} ${data.jobId}`);
});
}
ws://localhost:6790/ws
ws://localhost:6790/ws/queues/:queue

WebSocket supports 86 explicit event names across 19 namespaces, plus namespace and global wildcards. Clients subscribe to specific events and receive only matching data, zero polling needed.

Every pub/sub event follows this structure:

{
"event": "job:completed",
"ts": 1710000000000,
"data": {
"queue": "payments",
"jobId": "abc-123"
}
}
  • event, event name (category:action)
  • ts, unix timestamp in milliseconds
  • data, event-specific payload

After connecting, send a Subscribe command to start receiving events:

{
"cmd": "Subscribe",
"events": ["job:*", "queue:counts", "stats:snapshot", "health:status"],
"reqId": "1"
}

Response:

{
"ok": true,
"subscribed": ["job:*", "queue:counts", "stats:snapshot", "health:status"],
"reqId": "1"
}

Unsubscribe from specific events:

{ "cmd": "Unsubscribe", "events": ["job:progress"] }

Unsubscribe from everything:

{ "cmd": "Unsubscribe", "events": [] }
PatternMatches
*Every emitted event
job:*All 21 explicit job events, plus compatibility emissions described below
queue:*All 10 queue events, including queue:counts
flow:*Both flow events
worker:*All 7 worker events
dlq:*All 6 DLQ events
cron:*All 6 cron events
stats:*stats:snapshot
health:*health:status
storage:*All 5 storage events
config:*Both config events
ratelimit:*All 4 rate-limit events
concurrency:*All 3 concurrency events
webhook:*All 6 webhook events
batch:*Both batch events
client:*Both client events
auth:*auth:failed
cleanup:*Both cleanup events
server:*All 4 server events
memory:*memory:compacted

job:* and * may also receive job:waiting and job:duplicated from the legacy job-event bridge. They are compatibility emission names, not entries in the explicit subscription allow-list, so subscribing to either exact name is currently rejected. Use job:deduplicated for an exact deduplication subscription.

Clients that never send Subscribe receive all job events in the old format ({ eventType: "completed", queue, jobId, ... }). This maintains backward compatibility with existing integrations.

WebSocket clients can also send any TCP protocol command as JSON. This allows a dashboard to both receive events AND send commands (pause queue, retry job, etc.) over a single connection:

// Send a command
ws.send(JSON.stringify({ cmd: 'Pause', queue: 'emails', reqId: '2' }));
// Response
{ "ok": true, "reqId": "2" }

When AUTH_TOKENS is configured, the upgrade request must carry Authorization: Bearer <token>. A handshake without a valid token is rejected with 401 before the socket opens, so sending { "cmd": "Auth", "token": "..." } after connecting cannot replace header auth. The browser’s native WebSocket API cannot set this header.

When a WebSocket disconnects, all jobs owned by that client (pulled but not ACKed) are automatically released back to the queue. This prevents jobs from being stuck when a worker disconnects unexpectedly.

const ws = new WebSocket('ws://localhost:6790/ws');
ws.onopen = () => {
// Subscribe to everything a dashboard needs
ws.send(
JSON.stringify({
cmd: 'Subscribe',
events: [
'job:*', // All job lifecycle events
'queue:counts', // Real-time count updates (eliminates N+1 polling)
'stats:snapshot', // Global stats every 5s
'health:status', // Health check every 10s
'worker:*', // Worker connect/disconnect
'dlq:*', // DLQ events
'cron:*', // Cron events
'queue:paused', // Queue state changes
'queue:resumed',
],
})
);
};
ws.onmessage = (e) => {
const msg = JSON.parse(e.data);
// Pub/sub event
if (msg.event) {
switch (msg.event) {
// Periodic snapshots (replace HTTP polling)
case 'stats:snapshot':
updateOverviewCards(msg.data);
updateMetricsCharts(msg.data);
break;
case 'health:status':
updateConnectionBanner(msg.data.ok);
updateMemoryDisplay(msg.data.memory);
break;
// Queue counts (eliminates the N+1 problem)
case 'queue:counts':
updateQueueRow(msg.data.queue, msg.data);
break;
// Real-time activity feed
case 'job:completed':
case 'job:failed':
case 'job:pushed':
addToActivityFeed(msg);
break;
// Worker status
case 'worker:connected':
addWorkerRow(msg.data);
break;
case 'worker:disconnected':
removeWorkerRow(msg.data.workerId);
break;
// DLQ alerts
case 'dlq:added':
incrementDlqCounter(msg.data.queue);
showAlert(`Job ${msg.data.jobId} moved to DLQ: ${msg.data.reason}`);
break;
}
return;
}
// Command response (for interactive operations)
if (msg.reqId) {
handleCommandResponse(msg);
}
};
// Interactive: pause a queue from the dashboard
function pauseQueue(queue) {
ws.send(JSON.stringify({ cmd: 'Pause', queue, reqId: `pause-${queue}` }));
}

The following names can be supplied directly in a WebSocket Subscribe command. Payload fields listed with ? are present only on the path that has that information; the outer event envelope always supplies event, ts, and data.

EventPayloadDescription
job:pushedqueue, jobIdJob added to queue
job:activequeue, jobIdWorker picked up job
job:completedqueue, jobIdJob finished successfully
job:failedqueue, jobId, errorJob errored
job:removedqueue, jobId, prev?Job cancelled/deleted
job:promotedjobIdDelayed job moved to waiting
job:progressqueue, jobId, progressWorker reported progress (0-100)
job:delayedqueue, jobId, delayJob moved to delayed state
job:stalledqueue, jobId, stallCount?, action?Stall detected (no heartbeat)
job:retriedqueue, jobId, prev?Failed job retried
job:discardedjobIdJob sent to DLQ via discard
job:priority-changedjobId, newPriorityPriority updated
job:data-updatedjobIdJob payload modified
job:delay-changedjobId, newDelayDelay modified
job:timeoutqueue, jobId, timeoutActive job exceeded its processing timeout
job:lock-expiredqueue, jobId, renewalCountOwnership lock expired
job:deduplicatedqueue, jobId, strategyPush reused an existing deduplicated job
job:waiting-childrenqueue, jobId, dependsOn?Job is waiting for dependencies
job:dependencies-resolvedqueue, jobIdAll dependencies became complete
job:moved-to-delayedjobId, delayActive job was explicitly moved to delayed
job:expiredqueue, jobId, ttl, ageJob TTL expired (distinguished from fail)
EventPayloadDescription
queue:countsqueue, waiting, prioritized, active, completed, failed, delayedLatest counts after lifecycle activity; updates within 10ms are coalesced.
queue:pausedqueueQueue paused
queue:resumedqueueQueue resumed
queue:drainedqueue, countAll waiting/delayed jobs removed
queue:cleanedqueue, state, countJobs cleaned by state
queue:obliteratedqueueQueue destroyed
queue:createdqueueFirst job pushed to new queue
queue:removedqueueQueue removed
queue:idlequeue, idleSecondsQueue empty with no active jobs for N seconds. Configure via QUEUE_IDLE_THRESHOLD_MS (default: 30000).
queue:thresholdqueue, size, thresholdQueue size exceeds threshold. Configure via QUEUE_SIZE_THRESHOLD (default: 0 = disabled).
EventPayloadDescription
flow:completedparentJobId, queue, childrenCountAll children of a flow completed successfully
flow:failedparentJobId, failedChildId, queue, errorA child in a flow failed permanently (moved to DLQ)
EventPayloadDescription
dlq:addedqueue, jobId, reasonJob moved to DLQ
dlq:retriedqueue, jobId, countSingle DLQ entry retried
dlq:retry-allqueue, countAll DLQ entries retried
dlq:purgedqueue, countDLQ emptied
dlq:auto-retriedqueue, countMaintenance retried eligible DLQ entries
dlq:expiredqueue, countMaintenance purged expired DLQ entries
EventPayloadDescription
cron:createdname, queue, pattern?, every?, nextRunCron added
cron:deletednameCron removed
cron:firedname, queueCron triggered, job pushed
cron:updatedname, queue, pattern?, every?, nextRunCron modified
cron:missedname, queue, errorCron missed execution window
cron:skippedname, queue, reasonCron skipped due to overlap (previous instance still within interval)
EventPayloadDescription
worker:connectedworkerId, name, queues, hostname?, pid?Worker registered
worker:disconnectedworkerId, name?, clientId?Worker gone
worker:heartbeatworkerIdWorker alive signal
worker:idleworkerId, processedJobsWorker reached zero active jobs
worker:removed-staleworkerId, nameStale registration removed
worker:overloadedworkerId, name, activeJobs, concurrency, overloadedSecondsWorker at max concurrency for N seconds. Configure via WORKER_OVERLOAD_THRESHOLD_MS (default: 30000).
worker:errorworkerId, name, failedJobs, processedJobs, failureRateWorker failure rate is high (emitted at thresholds: 5, 10, 25, 50, 100 failures)
EventPayloadDescription
ratelimit:setqueue, maxRate limit configured
ratelimit:clearedqueueRate limit removed
ratelimit:hitclientIdTCP/HTTP client exceeded the protocol request limit
ratelimit:rejectedqueuePull found eligible work but the queue token bucket rejected it
concurrency:setqueue, concurrencyConcurrency limit configured
concurrency:clearedqueueConcurrency limit removed
concurrency:rejectedqueuePull found eligible work but no queue concurrency slot was available
EventPayloadDescription
webhook:addedid, url, eventsWebhook created
webhook:removedidWebhook deleted
webhook:firedwebhookId, url, eventWebhook delivered
webhook:failedwebhookId, url, event, errorWebhook delivery failed
webhook:enabledwebhookIdWebhook enabled without recreating it
webhook:disabledwebhookIdWebhook disabled without deleting it
EventPayloadDescription
batch:pushedqueue, total, inserted, duplicatesMulti-job push inserted at least one job
batch:pulledqueue, countBatch pull delivered more than one job
client:connectedclientId, transportTCP client connected
client:disconnectedclientId, transportTCP client disconnected
auth:failedclientId? or transportTCP command or HTTP authentication failed
cleanup:orphans-removedcountSilent active jobs recovered as stalls
cleanup:stale-deps-removedcountStale dependency entries removed

Periodic, Storage, Server & Memory (12 events)

Section titled “Periodic, Storage, Server & Memory (12 events)”
EventPayloadDescription
stats:snapshotwaiting, active, delayed, completed, dlq, totalPushed, totalCompleted, totalFailed, pushPerSec, pullPerSec, uptime, queues, workers, cronJobsEvery 5s
health:statusok, uptime, memory: { rss, heapUsed }, connectionsEvery 10s
storage:statuscollections, diskFullEvery 30s
storage:backup-startedbucketS3 backup started
storage:backup-completedbucket, keyS3 backup completed
storage:backup-failedbucket, errorS3 backup failed
storage:size-warningsizeMB, thresholdMBSQLite DB exceeds threshold. Configure via STORAGE_WARNING_MB (default: 0 = disabled).
server:startedtcpPort, httpPort, shardsServer listeners started
server:shutdownsignalGraceful shutdown began
server:recoveredqueues, jobsPersistent queues and jobs recovered on startup
server:memory-warningheapUsedMB, thresholdMB, rssMBHeap exceeds threshold. Configure via MEMORY_WARNING_MB (default: 0 = disabled).
memory:compacted(empty object)Manual memory compaction completed
EventPayloadDescription
config:stall-changedqueue, configStall detection config updated
config:dlq-changedqueue, configDLQ config updated

This is the most impactful event for dashboards. Job lifecycle activity schedules a refresh for the affected queue; refreshes within a 10ms window are coalesced, and the emitted payload contains the latest counts:

{
"event": "queue:counts",
"ts": 1710000000000,
"data": {
"queue": "payments",
"waiting": 15,
"prioritized": 4,
"active": 2,
"completed": 100,
"failed": 0,
"delayed": 3
}
}

Without queue:counts: A dashboard with 20 queues needs to poll GET /queues/:q/counts for each queue every few seconds = 200+ HTTP requests per minute.

With queue:counts: Subscribe once, receive real-time updates only when counts change. Zero polling, instant UI updates.


MethodPathDescription
POST/queues/:q/jobsPush a job
POST/queues/:q/jobs/bulkPush jobs in bulk
GET/queues/:q/jobsPull a job
POST/queues/:q/jobs/pull-batchPull jobs in batch
GET/jobs/:idGet job by ID
GET/jobs/custom/:customIdGet job by custom ID
DELETE/jobs/:idCancel a job
POST/jobs/:id/ackAcknowledge a job
POST/jobs/ack-batchAcknowledge batch
POST/jobs/:id/failFail a job
GET/jobs/:id/stateGet job state
GET/jobs/:id/resultGet job result
GET/jobs/:id/progressGet progress
POST/jobs/:id/progressUpdate progress
PUT/jobs/:id/dataUpdate job data
PUT/jobs/:id/priorityChange priority
POST/jobs/:id/promotePromote delayed job
POST/jobs/:id/move-to-waitMove to waiting
POST/jobs/:id/move-to-delayedMove to delayed
PUT/jobs/:id/delayChange delay
POST/jobs/:id/discardDiscard to DLQ
POST/jobs/:id/waitWait for completion
GET/jobs/:id/childrenGet children values
POST/jobs/:id/heartbeatJob heartbeat
POST/jobs/heartbeat-batchJob heartbeat batch
POST/jobs/:id/extend-lockExtend lock
POST/jobs/extend-locksExtend locks batch
GET/jobs/:id/logsGet logs
POST/jobs/:id/logsAdd log
DELETE/jobs/:id/logsClear logs
MethodPathDescription
GET/queuesList all queues
GET/queues/summaryAll queues with paused + counts
GET/queues/:q/workersWorkers for a queue
GET/queues/:q/jobs/listList jobs by state
GET/queues/:q/countsJob counts per state
GET/queues/:q/countQueued job count
GET/queues/:q/priority-countsCounts per priority
GET/queues/:q/pausedCheck if paused
POST/queues/:q/pausePause queue
POST/queues/:q/resumeResume queue
POST/queues/:q/drainDrain queue
POST/queues/:q/obliterateObliterate queue
POST/queues/:q/cleanClean old jobs
POST/queues/:q/promote-jobsPromote delayed jobs
POST/queues/:q/retry-completedRetry completed jobs
MethodPathDescription
GET/queues/:q/dlqList DLQ jobs
GET/queues/:q/dlq/statsDLQ statistics
POST/queues/:q/dlq/retryRetry DLQ jobs
POST/queues/:q/dlq/purgePurge DLQ
MethodPathDescription
PUT/queues/:q/rate-limitSet rate limit
DELETE/queues/:q/rate-limitClear rate limit
PUT/queues/:q/concurrencySet concurrency
DELETE/queues/:q/concurrencyClear concurrency
MethodPathDescription
GET/PUT/queues/:q/stall-configStall detection config
GET/PUT/queues/:q/dlq-configDLQ config
MethodPathDescription
GET/cronsList crons
POST/cronsAdd cron
GET/crons/:nameGet cron
DELETE/crons/:nameDelete cron
MethodPathDescription
GET/webhooksList webhooks
POST/webhooksAdd webhook
DELETE/webhooks/:idRemove webhook
PUT/webhooks/:id/enabledToggle webhook
MethodPathDescription
GET/workersList workers
POST/workersRegister worker
DELETE/workers/:idUnregister worker
POST/workers/:id/heartbeatWorker heartbeat
MethodPathAuthDescription
GET/healthNoHealth check
GET/healthzNoLiveness probe
GET/liveNoLiveness probe
GET/readyNoReadiness probe
GET/pingYesPing/pong
GET/statsYesServer statistics
GET/metricsYesThroughput metrics
GET/prometheusOptionalPrometheus metrics
GET/storageYesStorage health
POST/gcYesForce GC + compact
GET/heapstatsYesHeap statistics
MethodPathDescription
GET/dashboardAggregated overview
GET/dashboard/queuesPaginated queues with stats
GET/dashboard/queues/:qSingle queue detail
ProtocolPathDescription
SSE/eventsAll events (typed SSE events)
SSE/events/queues/:qQueue-filtered events
WebSocket/wsPub/sub + commands (86 explicit events, wildcards)
WebSocket/ws/queues/:qQueue-filtered pub/sub