Skip to content
Get started
Get started
TCP Protocol Reference: Binary MessagePack Commands
api reference · tcp

The wire protocol, documented.

A high-performance binary protocol on port 6789 by default. All messages use MessagePack encoding with length-prefixed framing, and pipelining lets the server process commands concurrently.

Every message (request and response) is wrapped in a length-prefixed frame:

Frame layoutrequest and response
payload length 4 bytes, big-endian unsigned 32-bit
MessagePack payload N bytes

The framing protocol works as follows:

  1. The first 4 bytes are a big-endian unsigned 32-bit integer indicating the length of the MessagePack payload.
  2. The next N bytes are the MessagePack-encoded command or response object.
  3. Maximum frame size is 64 MB. Frames exceeding this limit cause the connection to be terminated.
import { pack, unpack } from 'msgpackr';
// Encode a command into a framed message
function frameCommand(cmd: object): Uint8Array {
const payload = pack(cmd);
const frame = new Uint8Array(4 + payload.length);
// Write length prefix (big-endian u32)
frame[0] = (payload.length >> 24) & 0xff;
frame[1] = (payload.length >> 16) & 0xff;
frame[2] = (payload.length >> 8) & 0xff;
frame[3] = payload.length & 0xff;
frame.set(payload, 4);
return frame;
}
// Decode a framed response
function decodeFrame(frame: Uint8Array): object {
return unpack(frame);
}
import { pack, unpack } from 'msgpackr';
const socket = await Bun.connect({
hostname: 'localhost',
port: 6789,
socket: {
data(socket, data) {
// Parse frames from data, then unpack each frame with msgpackr
},
},
});
// Send a command
const cmd = pack({ cmd: 'Ping' });
const frame = new Uint8Array(4 + cmd.length);
frame[0] = (cmd.length >> 24) & 0xff;
frame[1] = (cmd.length >> 16) & 0xff;
frame[2] = (cmd.length >> 8) & 0xff;
frame[3] = cmd.length & 0xff;
frame.set(cmd, 4);
socket.write(frame);

Clients should send a Hello command after connecting to report their protocol revision and discover server capabilities.

Request:

{ cmd: 'Hello', protocolVersion: 3, capabilities: ['pipelining', 'separate-job-name'] }

Response:

{
ok: true,
protocolVersion: 3,
capabilities: ['pipelining', 'separate-job-name'],
server: 'bunqueue',
version: 'x.y.z' // Installed server package version
}

The current protocol version is 3. It supports pipelining and separate-job-name. Revision 3 places job metadata in top-level job.name and preserves job.data exactly as supplied. The server still accepts legacy inputs with no top-level name: at that inbound boundary only, a string data.name is decoded as the old embedded-name envelope.

The server supports pipelining: clients can send multiple commands without waiting for each response. The server processes frames in parallel with a concurrency limit of 50 commands per connection, controlled by a semaphore.

To correlate responses with requests when pipelining, include a reqId field in each command. The server echoes reqId back in the corresponding response.

// Send two commands simultaneously
socket.write(frameCommand({ cmd: 'PUSH', queue: 'emails', data: { to: 'a@b.com' }, reqId: '1' }));
socket.write(frameCommand({ cmd: 'PUSH', queue: 'emails', data: { to: 'c@d.com' }, reqId: '2' }));
// Responses may arrive in any order - match by reqId
// { ok: true, id: 'abc-123', reqId: '1' }
// { ok: true, id: 'def-456', reqId: '2' }

When the server is configured with AUTH_TOKENS, all connections must authenticate before sending other commands. The Auth command is always permitted regardless of authentication state.

Request:

{ cmd: 'Auth', token: 'your-secret-token' }

Response (success):

{ ok: true }

Response (failure):

{ ok: false, error: 'Invalid token' }

If auth tokens are configured and a client sends any command before authenticating, the server responds with:

{ ok: false, error: 'Not authenticated' }

All responses include an ok boolean field. On success ok is true with command-specific data. On failure ok is false with an error string.

// Success
{ ok: true, ...data, reqId?: string }
// Error
{ ok: false, error: 'Error message', reqId?: string }

SubscribeEvents selects one queue for the current connection; UnsubscribeEvents clears it without closing the socket. Both commands require normal authentication and return a regular reqId-correlated response.

{ cmd: 'SubscribeEvents', queue: 'tasks', reqId: 'events-1' }
{ ok: true, reqId: 'events-1' }
// Later, independently of command responses:
{ type: 'event', event: { eventType: 'completed', queue: 'tasks', jobId: '...', timestamp: 0, data: { ok: true } } }
{ cmd: 'UnsubscribeEvents', reqId: 'events-2' }

The unsolicited event envelope has no reqId. Pipelined clients must recognize type: 'event' before correlating command responses. A new subscription on the same connection replaces the previous queue. Slow subscribers are subject to the normal per-connection write-buffer limit.

When a TCP connection closes, the server automatically releases all jobs that were being processed by that client back to their queues. This uses retry logic with exponential backoff (up to 3 attempts) to ensure jobs are not left in an inconsistent state.

Each connection is subject to server-side rate limiting. If exceeded, the server responds with:

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

Every command object must include a cmd field. An optional reqId field can be included for request-response correlation (required for pipelining).

Add a single job to a queue.

Request:

{
cmd: 'PUSH',
queue: string, // Queue name (required, max 256 chars, alphanumeric/underscore/dash/dot/colon)
name: string, // Job name metadata (required for protocol v3 clients)
data: any, // Untouched user payload (required, max 10 MB)
priority?: number, // Higher = processed sooner (default: 0, range: -1000000 to 1000000)
delay?: number, // Delay in ms before processing (default: 0, max: 1 year)
maxAttempts?: number, // Max retry attempts (default: 3, range: 1-1000)
backoff?: number, // Retry backoff delay in ms (default: 1000, max: 1 day)
ttl?: number, // Time-to-live in ms (max: 1 year)
timeout?: number, // Processing timeout in ms (max: 1 day)
uniqueKey?: string, // Deduplication key
jobId?: string, // Custom job ID (idempotent)
dependsOn?: string[], // Job IDs this job depends on
tags?: string[], // Metadata tags
groupId?: string, // Job group identifier
lifo?: boolean, // Last-in-first-out (default: false)
removeOnComplete?: boolean, // Auto-remove on completion (default: false)
removeOnFail?: boolean, // Auto-remove on failure (default: false)
durable?: boolean, // Force immediate disk write, bypassing write buffer (default: false)
repeat?: { // Repeat configuration
every?: number, // Repeat interval in ms
pattern?: string, // Cron expression (alternative to every)
limit?: number, // Max repetitions
count?: number, // Current count
startDate?: number, // Don't fire before this timestamp
endDate?: number, // Don't fire after this timestamp
tz?: string, // IANA timezone for pattern
immediately?: boolean // Fire once on creation
},
// Flow / parent-child (used by FlowProducer):
parentId?: string, // Parent job ID
childrenIds?: string[], // Child job IDs (flow parent)
failParentOnFailure?: boolean,
removeDependencyOnFailure?: boolean,
ignoreDependencyOnFailure?: boolean,
continueParentOnFailure?: boolean,
// Advanced options:
stallTimeout?: number, // Stall detection timeout in ms (max: 1 day)
stackTraceLimit?: number, // Cap on stored stack trace lines
keepLogs?: number, // Cap on stored log entries
sizeLimit?: number, // Max serialized data size for this job
dedup?: { ttl?: number, extend?: boolean, replace?: boolean }, // Dedup options (uniqueKey carries the id)
debounceId?: string, // Debounce identifier
debounceTtl?: number, // Debounce window in ms
timestamp?: number // Explicit creation timestamp
}

The backoff field also accepts an object form: { type: 'fixed' | 'exponential', delay: number }.

Response:

{ ok: true, id: string } // The generated job ID (UUIDv7)

Batch push multiple jobs to a queue.

Request:

{
cmd: 'PUSHB',
queue: string,
jobs: Array<{
name: string,
data: any,
priority?: number,
delay?: number,
maxAttempts?: number,
backoff?: number,
ttl?: number,
timeout?: number,
uniqueKey?: string,
customId?: string,
dependsOn?: string[],
tags?: string[],
groupId?: string,
lifo?: boolean,
removeOnComplete?: boolean,
removeOnFail?: boolean,
durable?: boolean
}>
}

Each job is validated with the same rules as PUSH (option bounds and dependsOn existence). A dependsOn entry may also reference the customId of any job in the same batch, so order-independent intra-batch chains work. On violation the whole batch is rejected with an error naming the offending index (jobs[i]: ...).

Response:

{ ok: true, ids: string[] } // Array of generated job IDs

Atomically commit a fully resolved, potentially multi-queue FlowProducer graph. This is the command used by the Bun package and all six current official SDKs. Previously published clients may still compose legacy PUSH/UpdateParent calls.

Request:

{
cmd: 'PUSHF',
jobs: Array<{
id: string, // Final ID; non-empty, no colon
queue: string,
input: {
name: string,
data: unknown, // untouched user payload
dependsOn?: string[],
parentId?: string,
childrenIds?: string[],
// supported ordinary scheduling/retry/failure options
}
}>
}

The complete graph is validated before mutation: strict runtime types, duplicate/missing/asymmetric edges, cycles, policy conflicts, 10,000 jobs, 10 MB per job and 64 MB aggregate data. With configured SQLite, all job rows commit in one immediate transaction before any leaf becomes visible. In memory-only mode, publication is still atomic but not crash-durable.

Response:

{ ok: true, data: { jobs: Job[] } }

The returned array has exactly one authoritative committed snapshot per input ID. Any validation, ownership or persistence error returns { ok: false, error } and publishes no job.


Pull the next available job from a queue. Supports optional long polling and lock-based ownership.

Request:

{
cmd: 'PULL',
queue: string,
timeout?: number, // Long poll timeout in ms (0-60000, default: 0)
owner?: string, // Client identifier for lock-based pull
lockTtl?: number, // Lock TTL in ms (default: 30000)
detach?: boolean // Don't auto-release the job when this connection closes (CLI usage)
}

Response (without owner):

{ ok: true, job: Job | null }

Response (with owner, includes lock token):

{ ok: true, job: Job | null, token: string | null }

The token must be passed to ACK or FAIL to verify ownership.


Batch pull multiple jobs from a queue.

Request:

{
cmd: 'PULLB',
queue: string,
count: number, // Number of jobs to pull (1-1000)
timeout?: number, // Long poll timeout in ms (0-60000, default: 0), with or without owner
owner?: string, // Client identifier for lock-based pull
lockTtl?: number // Lock TTL in ms (default: 30000)
}

Response (without owner):

{ ok: true, jobs: Job[] }

Results are ordered by createdAt ascending (oldest first), with the job ID as a deterministic tie-breaker, before offset and limit are applied.

Response (with owner, includes lock tokens):

{ ok: true, jobs: Job[], tokens: string[] }

Acknowledge a job as completed.

Request:

{
cmd: 'ACK',
id: string, // Job ID
result?: any, // Optional result data
token?: string // Lock token (required if pulled with owner)
}

Response:

{ ok: true }

If an exact timeout or retired cron generation already finalized before the ACK claimed it, the response is a successful no-op rather than a retryable transport error:

{ ok: true, data: { applied: false, reason: 'already-finalized' } }

Batch acknowledge multiple jobs.

Request:

{
cmd: 'ACKB',
ids: string[], // Job IDs
results?: any[], // Optional results (same order as ids; if provided, length must match ids)
tokens?: string[] // Lock tokens (same order/length as ids; required for leased jobs)
}

The broker validates every token before completing any item. A missing or incorrect token rejects the whole batch and leaves all jobs, locks, and results unchanged.

Response:

{ ok: true }

A timeout may win after the batch’s lease preflight. Live positions still apply and the broker reports the exact ignored input positions in order:

{
ok: true,
data: {
ignoredIds: ['job-id'],
ignoredIndices: [2]
}
}

Clients must use ignoredIndices when IDs repeat. Wrong/missing tokens and ordinary missing/completed jobs remain errors.


Mark a job as failed. The job will be retried with exponential backoff if it has remaining attempts, otherwise it is moved to the dead-letter queue.

Request:

{
cmd: 'FAIL',
id: string, // Job ID
error?: string, // Error message
stack?: string[], // Failure stack trace lines, persisted server-side, capped at job.stackTraceLimit (#74)
unrecoverable?: boolean, // Skip all remaining retries and fail terminally (straight to DLQ)
token?: string // Lock token (required if pulled with owner)
}

The optional stack is stored on the job and surfaced by GetJob and on DLQ entries, so a failed job’s stack trace survives a restart.

Response:

{ ok: true }

An exact late generation uses the same successful no-op envelope as ACK:

{ ok: true, data: { applied: false, reason: 'already-finalized' } }

Retrieve a job by its internal ID.

Request:

{ cmd: 'GetJob', id: string }

Response:

{ ok: true, job: Job }

Returns an error if the job is not found.


Get the current state of a job.

Request:

{ cmd: 'GetState', id: string }

Response:

{ ok: true, id: string, state: string }

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


Get the stored result of a completed job.

Request:

{ cmd: 'GetResult', id: string }

Response:

{ ok: true, id: string, result: any }

The result field is the value passed via ACK. It may be null or undefined if no result was stored or if the result has been evicted from the LRU cache.


List jobs with filtering and pagination.

Request:

{
cmd: 'GetJobs',
queue: string,
state?: JobState | JobState[], // e.g. 'waiting', 'delayed', 'active', 'completed', 'failed', or an array
limit?: number, // Max results (default: 100)
offset?: number, // Skip N results (default: 0)
asc?: boolean // createdAt/id order (default: true)
}

Response:

{ ok: true, jobs: Job[] }

Ordering is applied before pagination. Send the same asc value on every request when traversing multiple offset pages.


Get job counts grouped by state for a specific queue.

Request:

{ cmd: 'GetJobCounts', queue: string }

Response:

{
ok: true,
counts: {
waiting: number,
prioritized: number,
delayed: number,
active: number,
completed: number,
failed: number,
'waiting-children': number,
paused: number
}
}

When the queue is paused, ready jobs are reported under paused instead of waiting/prioritized (BullMQ semantics).


Get job counts grouped by priority level for a specific queue.

Request:

{ cmd: 'GetCountsPerPriority', queue: string }

Response:

{ ok: true, queue: string, counts: Record<number, number> }

Look up a job by its custom ID (the jobId field from PUSH).

Request:

{ cmd: 'GetJobByCustomId', customId: string }

Response:

{ ok: true, job: Job }

Returns an error if no job with that custom ID exists.


Get the total number of jobs in a queue (all states).

Request:

{ cmd: 'Count', queue: string }

Response:

{ ok: true, count: number }

Get the progress of an active job.

Request:

{ cmd: 'GetProgress', id: string }

Response:

{ ok: true, progress: number, message: string | null }

Get the return values from all child jobs of a parent job. Used with FlowProducer workflows to retrieve results from completed children.

Request:

{ cmd: 'GetChildrenValues', id: string }

Response:

{ ok: true, data: { values: Record<string, any> } }

Returns an empty values object if the job has no children or if an error occurs.


Cancel a waiting or delayed job.

Request:

{ cmd: 'Cancel', id: string }

Response:

{ ok: true }

Update the progress of an active job.

Request:

{
cmd: 'Progress',
id: string,
progress: number, // 0-100
message?: string // Optional progress message
}

Response:

{ ok: true }

Update the data payload of an existing job.

Request:

{
cmd: 'Update',
id: string,
data: any // New job data
}

Response:

{ ok: true }

Change the priority of a queued job.

Request:

{
cmd: 'ChangePriority',
id: string,
priority: number,
lifo?: boolean // Tie-break ordering among same-priority jobs
}

Response:

{ ok: true }

Move a delayed job to the waiting state immediately.

Request:

{ cmd: 'Promote', id: string }

Response:

{ ok: true }

Move an active job back to the delayed state.

Request:

{
cmd: 'MoveToDelayed',
id: string,
delay: number, // Delay in ms from now
token?: string // Required when the active job has a lock
}

Response:

{ ok: true }

Discard a job by moving it to the dead-letter queue.

Request:

{ cmd: 'Discard', id: string, token?: string }

When the job has an active lease, token must match the current delivery token. For waiting or otherwise unlocked jobs, the field may be omitted for an administrative discard.

Response:

{ ok: true }

Wait for a job to complete. This is event-driven (no polling). Returns immediately if the job is already completed.

Request:

{
cmd: 'WaitJob',
id: string,
timeout?: number // Max wait time in ms (default: 30000, max: 600000)
}

Response:

{ ok: true, completed: boolean, result?: any }

Pause a queue. Workers will stop pulling new jobs.

Request:

{ cmd: 'Pause', queue: string }

Response:

{ ok: true }

Resume a paused queue.

Request:

{ cmd: 'Resume', queue: string }

Response:

{ ok: true }

Check whether a queue is currently paused.

Request:

{ cmd: 'IsPaused', queue: string }

Response:

{ ok: true, paused: boolean }

Remove all waiting jobs from a queue.

Request:

{ cmd: 'Drain', queue: string }

Response:

{ ok: true, count: number } // Number of jobs removed

Remove all data for a queue (all jobs in all states).

Request:

{ cmd: 'Obliterate', queue: string }

Response:

{ ok: true }

Remove jobs older than a grace period, optionally filtered by state.

Request:

{
cmd: 'Clean',
queue: string,
grace: number, // Grace period in ms - jobs older than this are removed
state?: string, // 'waiting'/'delayed'/'prioritized'/'paused' (queued jobs, the default), 'completed', or 'failed'
limit?: number // Max jobs to remove (default: 1000)
}

Response:

{ ok: true, count: number, ids: string[] } // IDs of the removed jobs

List the names of all known queues.

Request:

{ cmd: 'ListQueues' }

Response:

{ ok: true, queues: string[] } // Queue names

For per-queue counts use GetJobCounts per queue, or the HTTP GET /queues/summary endpoint.


Retrieve jobs from the dead-letter queue.

Request:

{
cmd: 'Dlq',
queue: string,
count?: number, // Max entries to return (optional)
filter?: {
reason?: string,
olderThan?: number,
newerThan?: number,
retriable?: boolean,
expired?: boolean,
limit?: number,
offset?: number
}
}

Response:

{ ok: true, jobs: Job[], entries: DlqEntry[] }

Read aggregate DLQ health for a queue.

{ cmd: 'GetDlqStats', queue: string }
{ ok: true, data: { stats: DlqStats } }

Retry jobs from the dead-letter queue (move them back to waiting).

Request:

{
cmd: 'RetryDlq',
queue: string,
jobId?: string, // Retry a specific job (optional; omit to retry all)
count?: number, // Cap the number of entries retried (omit = retry all)
filter?: DlqFilter // Retry only matching entries
}

Response:

{ ok: true, count: number } // Number of jobs retried

Clear all jobs from the dead-letter queue.

Request:

{ cmd: 'PurgeDlq', queue: string }

Response:

{ ok: true, count: number } // Number of jobs purged

Re-queue completed jobs back to waiting state.

Request:

{
cmd: 'RetryCompleted',
queue: string,
id?: string, // Retry a specific job (optional; omit to retry all)
count?: number, // Non-negative cap
timestamp?: number // completedAt must be <= this epoch-ms cutoff
}

Response:

{ ok: true, count: number }

Create or update a cron/repeating job schedule.

Request:

{
cmd: 'Cron',
name: string, // Unique cron job name
jobName?: string, // First-class name assigned to spawned jobs
queue: string, // Target queue
data: any, // Job data payload
schedule?: string, // Cron expression (e.g., '*/5 * * * *')
repeatEvery?: number, // Repeat interval in ms (alternative to schedule)
priority?: number, // Job priority
maxLimit?: number, // Max executions
timezone?: string, // IANA timezone (e.g., 'Europe/Rome', 'America/New_York')
uniqueKey?: string, // Deduplication key for cron-spawned jobs
dedup?: { ttl?: number, extend?: boolean, replace?: boolean }, // Dedup options for spawned jobs
skipMissedOnRestart?: boolean, // Skip missed runs on restart instead of executing them (default true)
immediately?: boolean, // Fire once on creation, then continue on schedule (default false)
skipIfNoWorker?: boolean, // Skip a tick when no worker is registered (default false)
preventOverlap?: boolean, // Skip a tick while the previous run is still pending/active (default true)
jobOptions?: { // Per-job options applied to every generated job
maxAttempts?: number,
backoff?: number | { type: 'fixed' | 'exponential', delay: number },
timeout?: number,
delay?: number,
stallTimeout?: number,
removeOnComplete?: boolean,
removeOnFail?: boolean
}
}

Response:

{
ok: true,
cron: {
name: string,
jobName: string,
queue: string,
schedule: string | null,
repeatEvery: number | null,
nextRun: number,
executions: number,
maxLimit: number | null,
timezone: string | null,
priority: number
}
}

Delete a cron job schedule by name.

Request:

{ cmd: 'CronDelete', name: string }

Response:

{ ok: true }

List all registered cron job schedules.

Request:

{ cmd: 'CronList' }

Response:

{
ok: true,
crons: Array<{
name: string,
jobName: string,
queue: string,
schedule: string | null,
repeatEvery: number | null,
nextRun: number,
executions: number,
maxLimit: number | undefined,
timezone: string | undefined
}>
}

Get a single cron job by name.

Request:

{ cmd: 'CronGet', name: string }

Response:

{
ok: true,
cron: {
name: string,
jobName: string,
queue: string,
schedule: string | null,
repeatEvery: number | null,
nextRun: number,
executions: number,
maxLimit: number | undefined,
timezone: string | undefined
}
}

Returns an error if the cron job is not found.


Connection health check.

Request:

{ cmd: 'Ping' }

Response:

{ ok: true, data: { pong: true, time: number } }

Protocol version negotiation and server capability discovery. See the Protocol Negotiation section above for details.

Request:

{
cmd: 'Hello',
protocolVersion: number,
capabilities?: Array<'pipelining' | 'separate-job-name'>
}

Response:

{
ok: true,
protocolVersion: number,
capabilities: Array<'pipelining' | 'separate-job-name'>,
server: 'bunqueue',
version: string
}

Get high-level server statistics.

Request:

{ cmd: 'Stats' }

Response:

{
ok: true,
stats: {
waiting: number, // Waiting jobs
active: number, // Active jobs
delayed: number, // Delayed jobs
dlq: number, // Dead-letter queue size
completed: number, // Completed count
failed: number, // Failed (totalFailed) count
uptime: number, // Server uptime in ms
pushPerSec: number, // Push throughput
pullPerSec: number // Pull throughput
}
}

Get detailed server metrics. The request without queue fields retains the legacy broker-wide response shown below.

Request:

{ cmd: 'Metrics' }

Response:

{
ok: true,
metrics: {
totalPushed: number,
totalPulled: number,
totalCompleted: number,
totalFailed: number,
avgLatencyMs: number,
avgProcessingMs: number,
memoryUsageMb: number,
sqliteSizeMb: number,
activeConnections: number
}
}

For durable queue-scoped minute metrics, send:

{
cmd: 'Metrics',
queue: 'emails',
type: 'completed', // or 'failed'
start: 0, // newest bucket index
end: -1 // through the oldest retained bucket
}
{
ok: true,
data: {
meta: { count: number, prevTS: number, prevCount: number },
data: number[], // one-minute buckets, newest first
count: number // bucket count before pagination
}
}

Keep only the newest lifecycle events for one queue. The response reports the exact removed count, so repeating the request at the same length returns zero.

{ cmd: 'TrimEvents', queue: 'emails', maxLength: 1000 }
{ ok: true, data: { removed: number } }

Get metrics in Prometheus text exposition format.

Request:

{ cmd: 'Prometheus' }

Response:

{ ok: true, data: { metrics: string } }

Get the storage/disk health status. Reports whether the disk is full or has errors.

Request:

{ cmd: 'StorageStatus' }

Response:

{
ok: true,
data: {
diskFull: boolean, // Whether the disk is full
error: string | null, // Error message if any
since: number | null // Timestamp when the issue started (ms since epoch)
}
}

Send a heartbeat for a registered worker (keeps the worker registration alive).

Request:

{
cmd: 'Heartbeat',
id: string, // Worker ID
activeJobs?: number, // Optional stats update
processed?: number,
failed?: number
}

Response:

{ ok: true, data: { ok: true } }

Send a heartbeat for an active job (prevents stall detection from marking it as stalled). Also renews the lock if a token is provided.

Request:

{
cmd: 'JobHeartbeat',
id: string, // Job ID
token?: string, // Lock token for renewal
duration?: number // Lock renewal duration in ms (with token: extends the lock)
}

Response:

{ ok: true, data: { ok: true } }

Batch job heartbeat for multiple active jobs.

Request:

{
cmd: 'JobHeartbeatB',
ids: string[], // Job IDs
tokens?: string[] // Lock tokens (same order as ids)
}

Response:

{ ok: true, data: { ok: true, count: number } }

Register a worker with the server for monitoring.

Request:

{
cmd: 'RegisterWorker',
name: string,
queues: string[], // Queues this worker processes
concurrency?: number,
workerId?: string, // Reuse a stable worker ID across reconnects
hostname?: string,
pid?: number,
startedAt?: number
}

Response:

{
ok: true,
data: {
workerId: string,
name: string,
queues: string[],
concurrency: number,
hostname: string | undefined,
pid: number | undefined,
status: 'active',
registeredAt: number,
lastSeen: number,
activeJobs: number,
processedJobs: number,
failedJobs: number,
currentJob: string | null
}
}

The registration is tied to the TCP connection: the server auto-unregisters the worker when the connection closes.


Remove a worker registration.

Request:

{ cmd: 'UnregisterWorker', workerId: string }

Response:

{ ok: true, data: { removed: true } }

List all registered workers and their stats.

Request:

{ cmd: 'ListWorkers' }

Response:

{
ok: true,
data: {
workers: Array<{
id: string,
name: string,
queues: string[],
concurrency: number,
hostname: string | undefined,
pid: number | undefined,
status: 'active' | 'stale', // stale = no heartbeat within WORKER_TIMEOUT_MS (default 30s)
registeredAt: number,
lastSeen: number,
activeJobs: number,
processedJobs: number,
failedJobs: number,
currentJob: string | null,
uptime: number
}>,
stats: object // Aggregated worker stats
}
}

Register a webhook to receive event notifications. URLs are validated to prevent SSRF (localhost, private IPs, and cloud metadata endpoints are blocked).

Request:

{
cmd: 'AddWebhook',
url: string, // Webhook URL (https required for production)
events: string[], // Event types to subscribe to
queue?: string, // Filter by queue (optional)
secret?: string // Signing secret for payload verification
}

Response:

{
ok: true,
data: {
webhookId: string,
url: string,
events: string[],
queue: string | undefined,
createdAt: number
}
}

Remove a registered webhook.

Request:

{ cmd: 'RemoveWebhook', webhookId: string }

Response:

{ ok: true, data: { removed: true } }

List all registered webhooks.

Request:

{ cmd: 'ListWebhooks' }

Response:

{
ok: true,
data: {
webhooks: Array<{
id: string,
url: string,
events: string[],
queue: string | undefined,
createdAt: number,
lastTriggered: number | null,
successCount: number,
failureCount: number,
enabled: boolean
}>,
stats: object
}
}

Set a rate limit on a queue: limit jobs per duration ms (default 1000, so jobs per second).

Request:

{
cmd: 'RateLimit',
queue: string,
limit: number, // Max jobs per window
duration?: number, // Window in ms (default 1000)
ttl?: number // Auto-expiry in ms: the server clears the limit itself
}

Invalid duration or ttl values (non-finite or not positive) fall back to the defaults (1 second window, permanent limit) instead of failing. Servers older than 2.8.35 ignore both optional fields.

Response:

{ ok: true }

Remove the rate limit from a queue.

Request:

{ cmd: 'RateLimitClear', queue: string }

Response:

{ ok: true }

Set a concurrency limit on a queue (max concurrent active jobs).

Request:

{
cmd: 'SetConcurrency',
queue: string,
limit: number
}

Response:

{ ok: true }

Remove the concurrency limit from a queue.

Request:

{ cmd: 'ClearConcurrency', queue: string }

Response:

{ ok: true }

Read the live rate/concurrency configuration and saturation state.

{ cmd: 'GetQueueLimits', queue: string, maxJobs?: number }
{
ok: true,
data: {
limits: {
rateLimit: { max: number, duration: number } | null,
rateLimitTtl: number, // -2 when no rate limit exists
concurrencyLimit: number | null,
maxed: boolean
}
}
}

{ cmd: 'GetDeduplicationJobId', queue: string, deduplicationId: string }
// -> { ok: true, data: { jobId: string | null } }
{ cmd: 'RemoveDeduplicationKey', queue: string, deduplicationId: string }
// -> { ok: true, data: { count: number } }
{ cmd: 'RemoveJobDeduplicationKey', id: string }
// -> { ok: true, data: { removed: boolean } }

The job-owned form removes a key only when the requested job is still its registered owner.


{ cmd: 'MoveToWaitingChildren', id: string, token?: string }
// -> { ok: true, data: { moved: true } }

The job must be active. The transition releases its active resources and persists the parked state. If the job has a lock, token must match it.


Add a log entry to a job.

Request:

{
cmd: 'AddLog',
id: string, // Job ID
message: string, // Log message
level?: 'info' | 'warn' | 'error' // Log level (default: 'info')
}

Response:

{ ok: true, data: { added: true } }

Get all log entries for a job.

Request:

{ cmd: 'GetLogs', id: string, start?: number, end?: number } // start/end: inclusive pagination indexes

Response:

{ ok: true, data: { logs: Array<{ message: string, level: string, timestamp: number }>, count: number } }

count is the total number of stored log entries (before pagination). Logs are capped at 100 entries per job.


Extend the lock TTL on an active job (lock-based processing).

Request:

{ cmd: 'ExtendLock', id: string, duration: number, token?: string }

Response: { ok: true } or { ok: false, error: 'Lock not found or invalid token' }


Batch variant of ExtendLock (positional arrays, same order).

Request:

{ cmd: 'ExtendLocks', ids: string[], tokens: string[], durations: number[] }

Response:

{ ok: true, count: number } // Number of locks successfully extended

Change the delay of a delayed job (recomputes runAt).

Request: { cmd: 'ChangeDelay', id: string, delay: number, token?: string }

token is required when the job is active and currently leased. Worker processor Job objects forward their current delivery token automatically; unlocked administrative transitions may omit it.

Response: { ok: true }


Move a job back to waiting, dispatching by current state: active is released back to the queue, delayed is promoted, failed is retried from the DLQ, waiting/prioritized is a no-op success.

Request: { cmd: 'MoveToWait', id: string, token?: string }

Response: { ok: true }

For an active locked job, token is required and must match the current lease. An active job without a lock can still be moved administratively.


Promote all (or up to count) delayed jobs in a queue to waiting.

Request: { cmd: 'PromoteJobs', queue: string, count?: number }

Response: { ok: true, count: number }


Clear a job’s log entries, optionally keeping the most recent N.

Request: { cmd: 'ClearLogs', id: string, keepLogs?: number }

Response: { ok: true }


Enable or disable a webhook without deleting it.

Request: { cmd: 'SetWebhookEnabled', id: string, enabled: boolean }

Response: { ok: true } or { ok: false, error: 'Webhook not found' }


Trigger internal memory compaction.

Request: { cmd: 'CompactMemory' }

Response: { ok: true }


Used by FlowProducer for parent/child job graphs.

Request: { cmd: 'UpdateParent', childId: string, parentId: string }

Response: { ok: true }

This is a compatibility command for legacy multi-request flow creation. If the parent already declares childId, only the child’s temporary parent marker is updated; the parent may be active or terminal and its state/topology is not rewritten. A queued, active, completed, DLQ, or removeOnComplete-tombstoned child is accepted when that declared edge is consistent. Persisted job/DLQ data and any failure-outbox key move atomically. A genuinely new edge still requires a queued parent; conflicting ownership, self-links, and undeclared missing nodes fail.

Request: { cmd: 'GetFailedChildrenValues', id: string }

Response: { ok: true, values: Record<string, any> }

Request: { cmd: 'GetIgnoredChildrenFailures', id: string }

Response: { ok: true, values: Record<string, any> }

Request: { cmd: 'RemoveChildDependency', id: string }

Response: { ok: true, removed: boolean }

Request: { cmd: 'RemoveUnprocessedChildren', id: string }

Response: { ok: true }


Per-queue stall detection configuration. Numeric fields: stallInterval, maxStalls, gracePeriod (numeric strings are coerced, non-numeric values are dropped).

Request:

{ cmd: 'SetStallConfig', queue: string, config: { stallInterval?: number, maxStalls?: number, gracePeriod?: number } }
{ cmd: 'GetStallConfig', queue: string }

Response: { ok: true } for set, { ok: true, config: {...} } for get.

Per-queue DLQ configuration. Numeric fields: autoRetryInterval, maxAutoRetries, maxAge, maxEntries.

Request:

{ cmd: 'SetDlqConfig', queue: string, config: { autoRetry?: boolean, autoRetryInterval?: number, maxAutoRetries?: number, maxAge?: number | null, maxEntries?: number } }
{ cmd: 'GetDlqConfig', queue: string }

Response: { ok: true } for set, { ok: true, config: {...} } for get.


Aggregated read-only snapshots for dashboards (same data as the HTTP /dashboard endpoints).

Request: { cmd: 'DashboardOverview' }

Response: { ok: true, data: { stats, throughput, latency, memory, collections, workers, crons, storage, timestamp } }

Request: { cmd: 'DashboardQueues' }

Response: { ok: true, data: { queues: Array<{ name, waiting, prioritized, delayed, active, dlq, paused }>, timestamp } }

Request: { cmd: 'DashboardQueue', queue: string, includeJobs?: boolean, jobsLimit?: number } (jobsLimit default 10, max 50)

Response: { ok: true, data: { name, counts, paused, priorityCounts, dlqPreview, jobs?, timestamp } }


Queue names must satisfy the following constraints:

  • Not empty and at most 256 characters
  • Only alphanumeric characters, underscores, dashes, dots, and colons: [a-zA-Z0-9_\-.:]+

Job data payloads are limited to 10 MB when serialized.

CategoryCommandDescription
CorePUSHAdd a job to a queue
PUSHBBatch push multiple jobs
PULLPull next job (supports long poll and locks)
PULLBBatch pull jobs
ACKAcknowledge job completion
ACKBBatch acknowledge
FAILMark job as failed
QueryGetJobGet job by ID
GetStateGet job state
GetResultGet job result
GetJobsList jobs with filtering
GetJobCountsCount jobs by state
GetCountsPerPriorityCount jobs by priority
GetJobByCustomIdLook up job by custom ID
CountTotal job count for a queue
GetProgressGet job progress
GetChildrenValuesGet child job return values
GetQueueLimitsRead live queue rate/concurrency status
GetDeduplicationJobIdResolve a queue-scoped deduplication key
ControlCancelCancel a job
ProgressUpdate job progress
UpdateUpdate job data
ChangePriorityChange job priority
PromoteMove delayed job to waiting
MoveToDelayedMove active job to delayed
MoveToWaitingChildrenPark an active job for children
ChangeDelayChange a delayed job’s delay
MoveToWaitMove a job back to waiting
PromoteJobsPromote all delayed jobs in a queue
DiscardMove job to DLQ
WaitJobWait for job completion
ExtendLockExtend a job lock
ExtendLocksExtend job locks (batch)
RemoveDeduplicationKeyRelease a queue-scoped deduplication key
RemoveJobDeduplicationKeyRelease only a job-owned key
PausePause a queue
ResumeResume a queue
IsPausedCheck if queue is paused
DrainRemove all waiting jobs
ObliterateRemove all queue data
CleanRemove old jobs
ListQueuesList all queues
DLQDlqGet DLQ entries
GetDlqStatsGet aggregate DLQ statistics
RetryDlqRetry DLQ jobs
PurgeDlqClear DLQ
RetryCompletedRe-queue completed jobs
CronCronCreate/update cron schedule
CronDeleteDelete cron schedule
CronListList cron schedules
CronGetGet cron schedule by name
MonitoringPingHealth check
HelloProtocol negotiation
StatsServer statistics
MetricsDetailed metrics
TrimEventsTrim one queue’s lifecycle journal
PrometheusPrometheus-format metrics
StorageStatusGet storage/disk health status
HeartbeatWorker heartbeat
JobHeartbeatJob heartbeat (stall prevention)
JobHeartbeatBBatch job heartbeat
WorkersRegisterWorkerRegister a worker
UnregisterWorkerUnregister a worker
ListWorkersList workers
WebhooksAddWebhookRegister a webhook
RemoveWebhookRemove a webhook
ListWebhooksList webhooks
SetWebhookEnabledEnable/disable a webhook
RateRateLimitSet queue rate limit
RateLimitClearClear queue rate limit
SetConcurrencySet queue concurrency limit
ClearConcurrencyClear concurrency limit
ConfigSetStallConfigSet per-queue stall config
GetStallConfigGet per-queue stall config
SetDlqConfigSet per-queue DLQ config
GetDlqConfigGet per-queue DLQ config
LogsAddLogAdd job log entry
GetLogsGet job logs
ClearLogsClear job logs
FlowUpdateParentUpdate a child’s parent reference
GetFailedChildrenValuesFailed children values
GetIgnoredChildrenFailuresIgnored children failures
RemoveChildDependencyRemove a child’s parent dependency
RemoveUnprocessedChildrenRemove unprocessed children
DashboardDashboardOverviewAggregated dashboard snapshot
DashboardQueuesAll queues with stats
DashboardQueueSingle queue detail
SystemCompactMemoryTrigger memory compaction
AuthAuthAuthenticate connection