- Docs
- Reference
- HTTP API
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" }Authentication
Section titled “Authentication”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 usagecurl -H "Authorization: Bearer secret-token-1" http://localhost:6790/statsThe 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:
| Endpoint | Reason |
|---|---|
GET /health | Load balancer health checks must work without credentials |
GET /healthz, GET /live | Kubernetes liveness probes |
GET /ready | Kubernetes readiness probes |
GET /prometheus | Public 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 ContentAccess-Control-Allow-Origin: *Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONSAccess-Control-Allow-Headers: Content-Type, AuthorizationAccess-Control-Max-Age: 86400The Max-Age: 86400 (24 hours) means browsers cache the preflight response, avoiding repeated OPTIONS requests.
Error Responses
Section titled “Error Responses”All errors follow a consistent format with appropriate HTTP status codes:
| Code | Meaning | When |
|---|---|---|
200 | Success | Operation completed successfully |
400 | Bad Request | Invalid JSON, missing required fields, validation failure (e.g., queue name too long, priority out of range) |
401 | Unauthorized | Missing or invalid Bearer token |
404 | Not Found | Unknown route, or job, cron, webhook, or worker not found |
429 | Rate Limited | Client exceeded the configured request rate |
500 | Internal Error | Unexpected server error (logged server-side) |
503 | Unavailable | Storage 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.addand flows, which rejects only what no job can run with: a non-number,NaN, an infinity in any field exceptmaxAttempts(where an infinity means the maximum attempt count), a negativetimeout,ttl,backofforbackoff.delay(a missingbackoff.delayis the 1000 ms default), and, withgroupId, aprioritythat is not an integer from 0 to 2,097,151. A negativedelaymakes the job ready at once with a past run time (as in 2.9.10), a move-to-delayed/delaydelayis required, and a lockdurationmust 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
jobIdfield)
Rate Limiting
Section titled “Rate Limiting”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".
| Variable | Default | Description |
|---|---|---|
RATE_LIMIT_WINDOW_MS | 60000 | Sliding window duration in milliseconds |
RATE_LIMIT_MAX_REQUESTS | 10000 | Maximum requests per window per IP. |
RATE_LIMIT_CLEANUP_MS | 60000 | Interval 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.
Job Lifecycle
Section titled “Job Lifecycle”Understanding the job lifecycle is essential for using the API effectively. A job flows through these states:
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.
Push a Job
Section titled “Push a Job”Add a new job to a queue. The job enters waiting state (or delayed if delay > 0).
POST /queues/:queue/jobscurl -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:
| Field | Type | Default | Description |
|---|---|---|---|
data | any | - | Job payload. Any JSON-serializable value. Max 10MB. A push without data is accepted and creates a job with no payload. |
name | string | "default" | Job name, stored as the top-level job.name. |
priority | number | 0 | Higher value = processed sooner. Any finite number; with groupId it is the group priority and must be an integer from 0 to 2,097,151. |
delay | number | 0 | Milliseconds before the job becomes available for processing. A negative delay makes it ready at once (past run time). |
maxAttempts | number | 3 | Maximum 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. |
backoff | number or object | 1000 | Base 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). |
ttl | number | - | Time-to-live from creation in milliseconds (0 or more). Job is discarded if not processed within this window. |
timeout | number | - | 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. |
uniqueKey | string | - | 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. |
jobId | string | - | 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. |
tags | string[] | [] | Metadata tags for filtering and querying. |
groupId | string | - | 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. |
lifo | boolean | false | Last-in-first-out ordering. When true, the job is processed before other jobs at the same priority. |
removeOnComplete | boolean | false | Automatically remove the job from memory after completion. Saves memory for fire-and-forget jobs. |
removeOnFail | boolean | false | Automatically remove the job after final failure (after all retries exhausted). |
durable | boolean | false | SQLite: 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. |
dependsOn | string[] | [] | Job IDs that must complete before this job becomes available. The job enters waiting-children state until all dependencies are met. |
repeat | object | - | 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:
| Status | Error | Cause |
|---|---|---|
400 | Invalid JSON body | Request body is not valid JSON |
404 | Not found | Empty queue name (no route matches) |
400 | Queue name contains invalid characters | Queue name has chars outside a-zA-Z0-9_-.: |
400 | Job data too large (max 10MB) | Serialized data exceeds 10MB |
400 | priority must be a number | Non-numeric priority ("high") |
400 | delay must be a number | Non-numeric delay (a negative one runs at once) |
400 | timeout must be at least 0 | Negative processing timeout |
400 | backoff.delay must be at least 0 | Negative object-form backoff.delay (a missing one is 1000) |
Push Jobs in Bulk
Section titled “Push Jobs in Bulk”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/bulkcurl -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 a Job
Section titled “Pull a Job”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 emptycurl http://localhost:6790/queues/emails/jobs
# Long-poll for up to 5 seconds, waits for a job to become availablecurl http://localhost:6790/queues/emails/jobs?timeout=5000| Parameter | Type | Default | Max | Description |
|---|---|---|---|---|
timeout | number | 0 | 60000 | Long-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
nulleven 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
nulleven if jobs exist (rate limit exceeded) - Per-group concurrency: if the job’s
groupIdhas reached its concurrency limit, the next job from a different group is returned
Pull Jobs in Batch
Section titled “Pull Jobs in Batch”Pull multiple jobs at once. More efficient than individual pulls for high-throughput workers.
POST /queues/:queue/jobs/pull-batchcurl -X POST http://localhost:6790/queues/emails/jobs/pull-batch \ -H "Content-Type: application/json" \ -d '{"count": 10, "timeout": 5000}'| Field | Type | Required | Range | Description |
|---|---|---|---|---|
count | number | Yes | 1-1000 | Number of jobs to pull |
timeout | number | No | 0-60000 | Long-poll timeout (ms), honored with or without owner. Default 0 (return immediately). |
owner | string | No | - | Lock owner identifier for lock-based processing |
lockTtl | number | No | - | 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.
Get a Job
Section titled “Get a Job”Retrieve a job by ID. Returns the full job object regardless of state (waiting, active, delayed, completed).
GET /jobs/:idcurl http://localhost:6790/jobs/019ce9d7-6983-7000-946f-48737be2b0f9Response (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" }
Get Job by Custom ID
Section titled “Get Job by Custom ID”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/:customIdcurl http://localhost:6790/jobs/custom/order-12345Returns the same response format as GET /jobs/:id.
Get Job State
Section titled “Get Job State”GET /jobs/:id/state{ "ok": true, "id": "019ce9d7-...", "state": "active" }Possible states: waiting, prioritized, delayed, active, waiting-children, completed, failed, unknown (job not found)
Get Job Result
Section titled “Get Job Result”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.
Cancel a Job
Section titled “Cancel a Job”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/:idcurl -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)" }(orfailed; on PostgreSQL also a waiting job that another live job depends on, withcurrent 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.
Acknowledge a Job
Section titled “Acknowledge a Job”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/ackcurl -X POST http://localhost:6790/jobs/019ce9d7-.../ack \ -H "Content-Type: application/json" \ -d '{"result": {"sent": true, "messageId": "abc-123"}}'Request body (optional):
| Field | Type | Description |
|---|---|---|
result | any | Completion result. Stored in LRU cache (10,000 max). |
token | string | Lock 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:
- Job is removed from the
activeprocessing queue - Result is stored in the LRU cache (if provided)
- Completion counter incremented
job:completedevent broadcast to all subscribersqueue:countsevent broadcast with updated counts- Dependent jobs (via
dependsOn) are checked and promoted if all dependencies are met - If
removeOnComplete: true, the job is permanently deleted from memory
Acknowledge Jobs in Batch
Section titled “Acknowledge Jobs in Batch”Acknowledge multiple jobs in a single round-trip.
POST /jobs/ack-batchcurl -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}]}'| Field | Type | Required | Description |
|---|---|---|---|
ids | string[] | Yes | Job IDs to acknowledge |
results | unknown[] | No | Per-job results (positional, same order as ids) |
tokens | string[] | No | Lock tokens (positional) |
Fail a Job
Section titled “Fail a Job”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/failcurl -X POST http://localhost:6790/jobs/019ce9d7-.../fail \ -H "Content-Type: application/json" \ -d '{"error": "SMTP connection refused"}'| Field | Type | Description |
|---|---|---|
error | string | Error message. Stored with the job for debugging. |
token | string | Lock token (if using lock-based processing). |
unrecoverable | boolean | Skip remaining retries and fail terminally (straight to DLQ). |
stack | string[] | Stack trace lines, stored on the job and its DLQ entry. |
Retry behavior:
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.
Update Job Data
Section titled “Update Job Data”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/datacurl -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 Job Priority
Section titled “Change Job Priority”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 }.
Promote a Delayed Job
Section titled “Promote a Delayed Job”Move a job from delayed to waiting state for immediate processing. The job becomes available for the next PULL operation.
POST /jobs/:id/promotecurl -X POST http://localhost:6790/jobs/019ce9d7-.../promoteError (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 to Waiting
Section titled “Move to Waiting”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 to Delayed
Section titled “Move to Delayed”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.
Change Delay
Section titled “Change Delay”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 }.
Discard to DLQ
Section titled “Discard to DLQ”Move a job directly to the Dead Letter Queue, bypassing the normal retry mechanism. Works on waiting, delayed, and active jobs.
POST /jobs/:id/discardcurl -X POST http://localhost:6790/jobs/019ce9d7-.../discardBroadcasts: job:discarded event.
Wait for Job Completion
Section titled “Wait for Job Completion”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/waitcurl -X POST http://localhost:6790/jobs/019ce9d7-.../wait \ -H "Content-Type: application/json" \ -d '{"timeout": 30000}'| Field | Type | Default | Description |
|---|---|---|---|
timeout | number | 30000 | Maximum 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.
Get/Update Job Progress
Section titled “Get/Update Job Progress”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.
Get Children Values
Section titled “Get Children Values”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 } } }}Job Heartbeat
Section titled “Job Heartbeat”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 Lock
Section titled “Extend Lock”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] }Job Logs
Section titled “Job Logs”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/logscurl -X POST http://localhost:6790/jobs/019ce9d7-.../logs \ -H "Content-Type: application/json" \ -d '{"message": "Connecting to SMTP server...", "level": "info"}'| Field | Type | Required | Description |
|---|---|---|---|
message | string | Yes | Log message |
level | string | No | info (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/logsClear logs:
DELETE /jobs/:id/logsQueues
Section titled “Queues”List All Queues
Section titled “List All Queues”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"] }Queues Summary
Section titled “Queues Summary”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).
List Workers for a Queue
Section titled “List Workers for a Queue”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 } ]}List Jobs by State
Section titled “List Jobs by State”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"| Parameter | Type | Default | Description |
|---|---|---|---|
status | string | all | State filter: waiting, prioritized, delayed, active, completed, failed, waiting-children. Aliases: state, states. Repeatable and comma-separated for multiple states. |
limit | number | 100 | Max jobs to return |
offset | number | 0 | Skip 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.
Get Job Counts
Section titled “Get Job Counts”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 }}Get Queued Count
Section titled “Get Queued Count”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 }Get Counts per Priority
Section titled “Get Counts per Priority”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 } }Check If Paused
Section titled “Check If Paused”GET /queues/:queue/paused{ "ok": true, "paused": false }Pause a Queue
Section titled “Pause a Queue”Stop processing new jobs from this queue. Active jobs continue to completion, only new pulls are blocked.
POST /queues/:queue/pauseBroadcasts: queue:paused event.
Resume a Queue
Section titled “Resume a Queue”Resume processing after a pause.
POST /queues/:queue/resumeBroadcasts: queue:resumed event.
Drain a Queue
Section titled “Drain a Queue”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 }.
Obliterate a Queue
Section titled “Obliterate a Queue”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/obliterateBroadcasts: queue:obliterated event.
Clean a Queue
Section titled “Clean a Queue”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/cleancurl -X POST http://localhost:6790/queues/emails/clean \ -H "Content-Type: application/json" \ -d '{"grace": 86400000, "state": "waiting", "limit": 500}'| Field | Type | Default | Description |
|---|---|---|---|
grace | number | 0 | Only remove jobs older than this many milliseconds. 0 = remove all. |
state | string | queued | waiting/delayed/prioritized/paused (all clean the queued set, the default), completed, or failed. |
limit | number | 1000 | Max 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 }.
Promote All Delayed Jobs
Section titled “Promote All Delayed Jobs”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.
Retry Completed Jobs
Section titled “Retry Completed 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.
Dead Letter Queue (DLQ)
Section titled “Dead Letter Queue (DLQ)”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.
List DLQ Jobs
Section titled “List DLQ Jobs”GET /queues/:queue/dlq[?limit=100&offset=0]| Parameter | Type | Default | Description |
|---|---|---|---|
limit | number | all | Max entries to return |
offset | number | 0 | Skip 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.
DLQ Stats
Section titled “DLQ Stats”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 }}Retry DLQ Jobs
Section titled “Retry DLQ Jobs”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.
Purge DLQ
Section titled “Purge DLQ”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 }.
Rate Limiting & Concurrency
Section titled “Rate Limiting & Concurrency”Per-queue controls for throughput and parallelism. These are queue-level settings, independent of HTTP rate limiting.
Set Rate Limit
Section titled “Set Rate Limit”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.
Clear Rate Limit
Section titled “Clear Rate Limit”DELETE /queues/:queue/rate-limitBroadcasts: ratelimit:cleared event.
Set Concurrency Limit
Section titled “Set Concurrency Limit”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.
Clear Concurrency Limit
Section titled “Clear Concurrency Limit”DELETE /queues/:queue/concurrencyBroadcasts: concurrency:cleared event.
Queue Configuration
Section titled “Queue Configuration”Stall Detection
Section titled “Stall Detection”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-configUpdate config:
PUT /queues/:queue/stall-config{ "config": { "stallInterval": 30000, "maxStalls": 3, "gracePeriod": 5000 }}| Field | Default | Description |
|---|---|---|
enabled | true | Turn stall detection on or off for this queue |
stallInterval | 30000 | A job is stalled after this many ms without a heartbeat |
maxStalls | 3 | Max times a job can stall before moving to DLQ |
gracePeriod | 5000 | Grace period after job starts before stall detection kicks in |
Broadcasts: config:stall-changed event.
DLQ Configuration
Section titled “DLQ Configuration”Get current config:
GET /queues/:queue/dlq-configUpdate config:
PUT /queues/:queue/dlq-config{ "config": { "autoRetry": true, "maxAge": 604800000, "maxEntries": 10000 }}| Field | Default | Description |
|---|---|---|
autoRetry | false | Automatically retry DLQ entries after a delay |
autoRetryInterval | 3600000 | Delay between automatic retries of an entry, in ms (default: 1 hour) |
maxAutoRetries | 3 | Automatic retries of an entry before auto-retry gives up on it |
maxAge | 604800000 | Max age of DLQ entries in ms (default: 7 days; null = never). Older entries are removed. |
maxEntries | 10000 | Max DLQ entries per queue. Oldest are evicted when full. |
Broadcasts: config:dlq-changed event.
Cron Jobs
Section titled “Cron Jobs”Schedule recurring jobs using cron expressions or fixed intervals.
List All Crons
Section titled “List All Crons”GET /crons{ "ok": true, "crons": [ { "name": "daily-cleanup", "queue": "maintenance", "schedule": "0 2 * * *", "repeatEvery": null, "nextRun": 1700100000000, "executions": 42, "maxLimit": null, "timezone": "UTC" } ]}Add a Cron Job
Section titled “Add a Cron Job”POST /cronscurl -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" }'| Field | Type | Required | Description |
|---|---|---|---|
name | string | Yes | Unique identifier. Re-using a name updates the existing cron. |
jobName | string | No | Name assigned to generated jobs (default "default"). |
queue | string | Yes | Target queue for the generated jobs. |
data | any | Yes | Job payload pushed on each execution. |
schedule | string | * | Cron expression ("*/5 * * * *", "0 2 * * *"). |
repeatEvery | number | * | Positive safe-integer interval in ms (alternative to cron expression). |
timezone | string | No | IANA timezone. Raw HTTP uses the server/system timezone when omitted. |
priority | number | No | Priority for generated jobs. |
maxLimit | number | No | Max total executions. Cron is removed after reaching this count. |
immediately | boolean | No | Fire once on creation, then continue on schedule (default false). |
skipIfNoWorker | boolean | No | Skip a tick when no worker is registered for the queue (default false). |
preventOverlap | boolean | No | Deduplicate overlapping runs, a tick is skipped while the previous generated job is still pending/active (default true). |
uniqueKey | string | No | Deduplication key for the generated jobs. |
dedup | object | No | Deduplication options for the generated jobs: { ttl?, extend?, replace? } (ttl finite). |
skipMissedOnRestart | boolean | No | Skip ticks missed while the server was down instead of running them on restart (default true). |
jobOptions | object | No | Per-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 a Cron Job
Section titled “Get a Cron Job”GET /crons/:nameDelete a Cron Job
Section titled “Delete a Cron Job”DELETE /crons/:nameBroadcasts: cron:deleted event.
Webhooks
Section titled “Webhooks”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).
List All Webhooks
Section titled “List All Webhooks”GET /webhooksAdd a Webhook
Section titled “Add a Webhook”POST /webhookscurl -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" }'| Field | Type | Required | Description |
|---|---|---|---|
url | string | Yes | HTTP or HTTPS endpoint URL. Validated against SSRF (localhost, private IPs, cloud metadata blocked). |
events | string[] | Yes | Event types: job.pushed, job.started, job.completed, job.failed, job.progress. Other values are rejected. |
queue | string | No | Filter to specific queue. Omit for all queues. |
secret | string | No | HMAC 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.
Remove a Webhook
Section titled “Remove a Webhook”DELETE /webhooks/:idBroadcasts: webhook:removed event.
Enable/Disable a Webhook
Section titled “Enable/Disable a Webhook”PUT /webhooks/:id/enabled{ "enabled": false }Disabled webhooks stop receiving deliveries but retain their configuration.
Workers
Section titled “Workers”List All Workers
Section titled “List All Workers”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 } }}Register a Worker
Section titled “Register a Worker”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 }.
Unregister a Worker
Section titled “Unregister a Worker”DELETE /workers/:idBroadcasts: worker:disconnected event with { workerId }.
Worker Heartbeat
Section titled “Worker Heartbeat”Keep a worker’s registration alive. Workers that stop sending heartbeats are eventually marked as disconnected.
POST /workers/:id/heartbeatMonitoring
Section titled “Monitoring”Health Check
Section titled “Health Check”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).
Liveness / Readiness Probes
Section titled “Liveness / Readiness Probes”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 }}Metrics (JSON)
Section titled “Metrics (JSON)”GET /metrics{ "ok": true, "metrics": { "totalPushed": 100000, "totalPulled": 99500, "totalCompleted": 98000, "totalFailed": 200 }}Prometheus Metrics
Section titled “Prometheus Metrics”GET /prometheusReturns text/plain; version=0.0.4 format for Prometheus scraping. Includes per-queue gauges, throughput counters, and latency histograms. Optionally requires auth (requireAuthForMetrics).
Storage Status
Section titled “Storage Status”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.
Force Garbage Collection
Section titled “Force Garbage Collection”POST /gcTriggers 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 }}Heap Stats
Section titled “Heap Stats”GET /heapstatsDetailed V8/JSC heap breakdown for debugging memory leaks. Returns top 20 object types by count, internal collection sizes, and heap metrics.
Dashboard Endpoints
Section titled “Dashboard Endpoints”Aggregated read-only snapshots designed for dashboards (fewer round-trips than composing the individual endpoints).
Overview
Section titled “Overview”GET /dashboardSingle 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.
Queues (paginated)
Section titled “Queues (paginated)”GET /dashboard/queues[?limit=100&offset=0]| Parameter | Type | Default | Description |
|---|---|---|---|
limit | number | 100 | Max queues to return (1-500) |
offset | number | 0 | Skip 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}Queue Detail
Section titled “Queue Detail”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).
Real-time Events
Section titled “Real-time Events”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.
Server-Sent Events (SSE)
Section titled “Server-Sent Events (SSE)”GET /eventsGET /events/queues/:queueSSE 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}`); });}WebSocket Pub/Sub
Section titled “WebSocket Pub/Sub”ws://localhost:6790/wsws://localhost:6790/ws/queues/:queueWebSocket 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.
Event Format
Section titled “Event Format”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 millisecondsdata, event-specific payload
Subscribe / Unsubscribe
Section titled “Subscribe / Unsubscribe”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": [] }Wildcards
Section titled “Wildcards”| Pattern | Matches |
|---|---|
* | 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.
Legacy Mode
Section titled “Legacy Mode”Clients that never send Subscribe receive all job events in the old format ({ eventType: "completed", queue, jobId, ... }). This maintains backward compatibility with existing integrations.
Sending Commands
Section titled “Sending Commands”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 commandws.send(JSON.stringify({ cmd: 'Pause', queue: 'emails', reqId: '2' }));
// Response{ "ok": true, "reqId": "2" }Authentication
Section titled “Authentication”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.
Connection Cleanup
Section titled “Connection Cleanup”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.
Complete Dashboard Example
Section titled “Complete Dashboard Example”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 dashboardfunction pauseQueue(queue) { ws.send(JSON.stringify({ cmd: 'Pause', queue, reqId: `pause-${queue}` }));}Explicit Subscription Events (86)
Section titled “Explicit Subscription Events (86)”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.
Job Lifecycle (21 events)
Section titled “Job Lifecycle (21 events)”| Event | Payload | Description |
|---|---|---|
job:pushed | queue, jobId | Job added to queue |
job:active | queue, jobId | Worker picked up job |
job:completed | queue, jobId | Job finished successfully |
job:failed | queue, jobId, error | Job errored |
job:removed | queue, jobId, prev? | Job cancelled/deleted |
job:promoted | jobId | Delayed job moved to waiting |
job:progress | queue, jobId, progress | Worker reported progress (0-100) |
job:delayed | queue, jobId, delay | Job moved to delayed state |
job:stalled | queue, jobId, stallCount?, action? | Stall detected (no heartbeat) |
job:retried | queue, jobId, prev? | Failed job retried |
job:discarded | jobId | Job sent to DLQ via discard |
job:priority-changed | jobId, newPriority | Priority updated |
job:data-updated | jobId | Job payload modified |
job:delay-changed | jobId, newDelay | Delay modified |
job:timeout | queue, jobId, timeout | Active job exceeded its processing timeout |
job:lock-expired | queue, jobId, renewalCount | Ownership lock expired |
job:deduplicated | queue, jobId, strategy | Push reused an existing deduplicated job |
job:waiting-children | queue, jobId, dependsOn? | Job is waiting for dependencies |
job:dependencies-resolved | queue, jobId | All dependencies became complete |
job:moved-to-delayed | jobId, delay | Active job was explicitly moved to delayed |
job:expired | queue, jobId, ttl, age | Job TTL expired (distinguished from fail) |
Queue (10 events)
Section titled “Queue (10 events)”| Event | Payload | Description |
|---|---|---|
queue:counts | queue, waiting, prioritized, active, completed, failed, delayed | Latest counts after lifecycle activity; updates within 10ms are coalesced. |
queue:paused | queue | Queue paused |
queue:resumed | queue | Queue resumed |
queue:drained | queue, count | All waiting/delayed jobs removed |
queue:cleaned | queue, state, count | Jobs cleaned by state |
queue:obliterated | queue | Queue destroyed |
queue:created | queue | First job pushed to new queue |
queue:removed | queue | Queue removed |
queue:idle | queue, idleSeconds | Queue empty with no active jobs for N seconds. Configure via QUEUE_IDLE_THRESHOLD_MS (default: 30000). |
queue:threshold | queue, size, threshold | Queue size exceeds threshold. Configure via QUEUE_SIZE_THRESHOLD (default: 0 = disabled). |
Flow (2 events)
Section titled “Flow (2 events)”| Event | Payload | Description |
|---|---|---|
flow:completed | parentJobId, queue, childrenCount | All children of a flow completed successfully |
flow:failed | parentJobId, failedChildId, queue, error | A child in a flow failed permanently (moved to DLQ) |
DLQ (6 events)
Section titled “DLQ (6 events)”| Event | Payload | Description |
|---|---|---|
dlq:added | queue, jobId, reason | Job moved to DLQ |
dlq:retried | queue, jobId, count | Single DLQ entry retried |
dlq:retry-all | queue, count | All DLQ entries retried |
dlq:purged | queue, count | DLQ emptied |
dlq:auto-retried | queue, count | Maintenance retried eligible DLQ entries |
dlq:expired | queue, count | Maintenance purged expired DLQ entries |
Cron (6 events)
Section titled “Cron (6 events)”| Event | Payload | Description |
|---|---|---|
cron:created | name, queue, pattern?, every?, nextRun | Cron added |
cron:deleted | name | Cron removed |
cron:fired | name, queue | Cron triggered, job pushed |
cron:updated | name, queue, pattern?, every?, nextRun | Cron modified |
cron:missed | name, queue, error | Cron missed execution window |
cron:skipped | name, queue, reason | Cron skipped due to overlap (previous instance still within interval) |
Worker (7 events)
Section titled “Worker (7 events)”| Event | Payload | Description |
|---|---|---|
worker:connected | workerId, name, queues, hostname?, pid? | Worker registered |
worker:disconnected | workerId, name?, clientId? | Worker gone |
worker:heartbeat | workerId | Worker alive signal |
worker:idle | workerId, processedJobs | Worker reached zero active jobs |
worker:removed-stale | workerId, name | Stale registration removed |
worker:overloaded | workerId, name, activeJobs, concurrency, overloadedSeconds | Worker at max concurrency for N seconds. Configure via WORKER_OVERLOAD_THRESHOLD_MS (default: 30000). |
worker:error | workerId, name, failedJobs, processedJobs, failureRate | Worker failure rate is high (emitted at thresholds: 5, 10, 25, 50, 100 failures) |
Rate Limiting & Concurrency (7 events)
Section titled “Rate Limiting & Concurrency (7 events)”| Event | Payload | Description |
|---|---|---|
ratelimit:set | queue, max | Rate limit configured |
ratelimit:cleared | queue | Rate limit removed |
ratelimit:hit | clientId | TCP/HTTP client exceeded the protocol request limit |
ratelimit:rejected | queue | Pull found eligible work but the queue token bucket rejected it |
concurrency:set | queue, concurrency | Concurrency limit configured |
concurrency:cleared | queue | Concurrency limit removed |
concurrency:rejected | queue | Pull found eligible work but no queue concurrency slot was available |
Webhook (6 events)
Section titled “Webhook (6 events)”| Event | Payload | Description |
|---|---|---|
webhook:added | id, url, events | Webhook created |
webhook:removed | id | Webhook deleted |
webhook:fired | webhookId, url, event | Webhook delivered |
webhook:failed | webhookId, url, event, error | Webhook delivery failed |
webhook:enabled | webhookId | Webhook enabled without recreating it |
webhook:disabled | webhookId | Webhook disabled without deleting it |
Batch, Client, Auth & Cleanup (7 events)
Section titled “Batch, Client, Auth & Cleanup (7 events)”| Event | Payload | Description |
|---|---|---|
batch:pushed | queue, total, inserted, duplicates | Multi-job push inserted at least one job |
batch:pulled | queue, count | Batch pull delivered more than one job |
client:connected | clientId, transport | TCP client connected |
client:disconnected | clientId, transport | TCP client disconnected |
auth:failed | clientId? or transport | TCP command or HTTP authentication failed |
cleanup:orphans-removed | count | Silent active jobs recovered as stalls |
cleanup:stale-deps-removed | count | Stale dependency entries removed |
Periodic, Storage, Server & Memory (12 events)
Section titled “Periodic, Storage, Server & Memory (12 events)”| Event | Payload | Description |
|---|---|---|
stats:snapshot | waiting, active, delayed, completed, dlq, totalPushed, totalCompleted, totalFailed, pushPerSec, pullPerSec, uptime, queues, workers, cronJobs | Every 5s |
health:status | ok, uptime, memory: { rss, heapUsed }, connections | Every 10s |
storage:status | collections, diskFull | Every 30s |
storage:backup-started | bucket | S3 backup started |
storage:backup-completed | bucket, key | S3 backup completed |
storage:backup-failed | bucket, error | S3 backup failed |
storage:size-warning | sizeMB, thresholdMB | SQLite DB exceeds threshold. Configure via STORAGE_WARNING_MB (default: 0 = disabled). |
server:started | tcpPort, httpPort, shards | Server listeners started |
server:shutdown | signal | Graceful shutdown began |
server:recovered | queues, jobs | Persistent queues and jobs recovered on startup |
server:memory-warning | heapUsedMB, thresholdMB, rssMB | Heap exceeds threshold. Configure via MEMORY_WARNING_MB (default: 0 = disabled). |
memory:compacted | (empty object) | Manual memory compaction completed |
Config (2 events)
Section titled “Config (2 events)”| Event | Payload | Description |
|---|---|---|
config:stall-changed | queue, config | Stall detection config updated |
config:dlq-changed | queue, config | DLQ config updated |
The queue:counts Event
Section titled “The queue:counts Event”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.
Endpoint Summary
Section titled “Endpoint Summary”Jobs (30 endpoints)
Section titled “Jobs (30 endpoints)”| Method | Path | Description |
|---|---|---|
POST | /queues/:q/jobs | Push a job |
POST | /queues/:q/jobs/bulk | Push jobs in bulk |
GET | /queues/:q/jobs | Pull a job |
POST | /queues/:q/jobs/pull-batch | Pull jobs in batch |
GET | /jobs/:id | Get job by ID |
GET | /jobs/custom/:customId | Get job by custom ID |
DELETE | /jobs/:id | Cancel a job |
POST | /jobs/:id/ack | Acknowledge a job |
POST | /jobs/ack-batch | Acknowledge batch |
POST | /jobs/:id/fail | Fail a job |
GET | /jobs/:id/state | Get job state |
GET | /jobs/:id/result | Get job result |
GET | /jobs/:id/progress | Get progress |
POST | /jobs/:id/progress | Update progress |
PUT | /jobs/:id/data | Update job data |
PUT | /jobs/:id/priority | Change priority |
POST | /jobs/:id/promote | Promote delayed job |
POST | /jobs/:id/move-to-wait | Move to waiting |
POST | /jobs/:id/move-to-delayed | Move to delayed |
PUT | /jobs/:id/delay | Change delay |
POST | /jobs/:id/discard | Discard to DLQ |
POST | /jobs/:id/wait | Wait for completion |
GET | /jobs/:id/children | Get children values |
POST | /jobs/:id/heartbeat | Job heartbeat |
POST | /jobs/heartbeat-batch | Job heartbeat batch |
POST | /jobs/:id/extend-lock | Extend lock |
POST | /jobs/extend-locks | Extend locks batch |
GET | /jobs/:id/logs | Get logs |
POST | /jobs/:id/logs | Add log |
DELETE | /jobs/:id/logs | Clear logs |
Queues (15 endpoints)
Section titled “Queues (15 endpoints)”| Method | Path | Description |
|---|---|---|
GET | /queues | List all queues |
GET | /queues/summary | All queues with paused + counts |
GET | /queues/:q/workers | Workers for a queue |
GET | /queues/:q/jobs/list | List jobs by state |
GET | /queues/:q/counts | Job counts per state |
GET | /queues/:q/count | Queued job count |
GET | /queues/:q/priority-counts | Counts per priority |
GET | /queues/:q/paused | Check if paused |
POST | /queues/:q/pause | Pause queue |
POST | /queues/:q/resume | Resume queue |
POST | /queues/:q/drain | Drain queue |
POST | /queues/:q/obliterate | Obliterate queue |
POST | /queues/:q/clean | Clean old jobs |
POST | /queues/:q/promote-jobs | Promote delayed jobs |
POST | /queues/:q/retry-completed | Retry completed jobs |
DLQ (4 endpoints)
Section titled “DLQ (4 endpoints)”| Method | Path | Description |
|---|---|---|
GET | /queues/:q/dlq | List DLQ jobs |
GET | /queues/:q/dlq/stats | DLQ statistics |
POST | /queues/:q/dlq/retry | Retry DLQ jobs |
POST | /queues/:q/dlq/purge | Purge DLQ |
Rate Limiting & Concurrency (4 endpoints)
Section titled “Rate Limiting & Concurrency (4 endpoints)”| Method | Path | Description |
|---|---|---|
PUT | /queues/:q/rate-limit | Set rate limit |
DELETE | /queues/:q/rate-limit | Clear rate limit |
PUT | /queues/:q/concurrency | Set concurrency |
DELETE | /queues/:q/concurrency | Clear concurrency |
Configuration (4 endpoints)
Section titled “Configuration (4 endpoints)”| Method | Path | Description |
|---|---|---|
GET/PUT | /queues/:q/stall-config | Stall detection config |
GET/PUT | /queues/:q/dlq-config | DLQ config |
Crons (4 endpoints)
Section titled “Crons (4 endpoints)”| Method | Path | Description |
|---|---|---|
GET | /crons | List crons |
POST | /crons | Add cron |
GET | /crons/:name | Get cron |
DELETE | /crons/:name | Delete cron |
Webhooks (4 endpoints)
Section titled “Webhooks (4 endpoints)”| Method | Path | Description |
|---|---|---|
GET | /webhooks | List webhooks |
POST | /webhooks | Add webhook |
DELETE | /webhooks/:id | Remove webhook |
PUT | /webhooks/:id/enabled | Toggle webhook |
Workers (4 endpoints)
Section titled “Workers (4 endpoints)”| Method | Path | Description |
|---|---|---|
GET | /workers | List workers |
POST | /workers | Register worker |
DELETE | /workers/:id | Unregister worker |
POST | /workers/:id/heartbeat | Worker heartbeat |
Monitoring (11 endpoints)
Section titled “Monitoring (11 endpoints)”| Method | Path | Auth | Description |
|---|---|---|---|
GET | /health | No | Health check |
GET | /healthz | No | Liveness probe |
GET | /live | No | Liveness probe |
GET | /ready | No | Readiness probe |
GET | /ping | Yes | Ping/pong |
GET | /stats | Yes | Server statistics |
GET | /metrics | Yes | Throughput metrics |
GET | /prometheus | Optional | Prometheus metrics |
GET | /storage | Yes | Storage health |
POST | /gc | Yes | Force GC + compact |
GET | /heapstats | Yes | Heap statistics |
Dashboard (3 endpoints)
Section titled “Dashboard (3 endpoints)”| Method | Path | Description |
|---|---|---|
GET | /dashboard | Aggregated overview |
GET | /dashboard/queues | Paginated queues with stats |
GET | /dashboard/queues/:q | Single queue detail |
Real-time (4 channels, 86 pub/sub events)
Section titled “Real-time (4 channels, 86 pub/sub events)”| Protocol | Path | Description |
|---|---|---|
| SSE | /events | All events (typed SSE events) |
| SSE | /events/queues/:q | Queue-filtered events |
| WebSocket | /ws | Pub/sub + commands (86 explicit events, wildcards) |
| WebSocket | /ws/queues/:q | Queue-filtered pub/sub |