Queues
Queues route runs and own durable execution policy.
A queue is provider-neutral runtime infrastructure policy. It names where work is routed and, when configured, how much of that work may execute at once.
Queue names are persisted as stable strings on runs, events, outbox messages, cursors, and transport envelopes. TypeScript authoring APIs use queue definitions so task code, worker filters, trigger overrides, schedules, and provider bindings do not drift.
Use queues for workload routing such as default, emails, billing, media, and imports. Use separate lanes for real infrastructure boundaries such as a different database, AWS account, region, tenant boundary, compliance boundary, or durability policy.
Define queues once and export them as a catalog:
import { queue } from '@runlane/core'
export const mainQueue = queue({ name: 'main', default: true })
export const invoiceSyncQueue = queue({
name: 'invoice_sync',
concurrencyLimit: 1,
dispatchTimeout: '2m',
})
export const queues = [mainQueue, invoiceSyncQueue]Register the same definitions with the runtime:
const runlane = createRunlane({
lane,
queues,
tasks: { syncInvoices },
})queue() options:
| Option | Required | Default | What it controls |
|---|---|---|---|
name | Yes | None | Stable logical queue name persisted on runs, events, outbox rows, cursors, and transport messages. It must be a non-empty Runlane id and cannot contain :. |
default | No | false | Marks the queue selected when run creation has no task queue or trigger override. At most one runtime queue can be default. |
concurrencyLimit | No | Unbounded | Durable max occupied slots per queue capacity partition when storage reports enforcesQueueConcurrency. |
dispatchTimeout | No | None | How long a bounded-queue dispatch reservation may sit unclaimed before maintenance can make that capacity available again. Requires concurrencyLimit. |
Tasks, schedules, triggers, workers, and operator queue overrides should pass queue definitions:
const syncInvoices = task({
id: 'invoices.sync',
queue: invoiceSyncQueue,
schema,
async run(payload) {
await syncProvider(payload)
},
})
const worker = runlane.worker({
queues: [invoiceSyncQueue],
concurrency: 4,
})Omitting queues on runlane.worker() or executeNext() means the worker can scan every registered runtime queue. Passing queues: [invoiceSyncQueue] is a filter: that worker only claims runs selected for the registered invoice_sync queue.
Defaults And Drift
createRunlane({ queues }) is the authoritative queue registry. Queue names must be unique, and at most one queue may be marked default: true.
Tasks, schedules, and trigger calls may select a queue explicitly. When run creation has no task queue and no trigger override, runtime resolution uses the default queue. If the runtime has no default queue, that path fails fast with a configuration error.
There is no lane-invented fallback queue. If a runtime has no default queue, callers must select a queue explicitly.
Runtime APIs reject queue definitions whose name is missing from the registry or whose registered policy differs. This catches drift between shared queue constants, task definitions, worker filters, operator filters, and provider bindings.
Durable Capacity
concurrencyLimit is durable queue policy enforced by storage. A bounded queue occupies capacity with queued runs that still have an active dispatch reservation and running or cancellation-requested runs that still have an active lease. Storage uses that occupied count before appending new bounded-queue delivery requests or returning runnable candidates.
dispatchTimeout controls how long a dispatch reservation may sit unclaimed before maintenance can make capacity available again. It is only valid when concurrencyLimit is set. If omitted on a bounded queue, dispatch recovery uses the contract lease duration as the reservation window.
Worker concurrency is local process capacity. Queue concurrencyLimit is durable storage capacity. Use both when appropriate: a worker may have 10 local slots while a queue allows only 2 active attempts for a resource partition.
concurrencyKey partitions a bounded queue. Without a concurrencyKey, the whole queue is one capacity partition. With a key, each distinct key gets its own concurrencyLimit.
Provider Bindings
Provider-specific settings live at lane setup, not in task definitions. For SQS:
import { sqsQueue } from '@runlane/transport-sqs'
const lane = postgresSqsLane({
postgres,
sqs: {
client,
queues: [
sqsQueue(invoiceSyncQueue, {
queueUrl: process.env.RUNLANE_INVOICE_SYNC_QUEUE_URL,
}),
],
},
})Each SQS binding provides exactly one of queueUrl or queueName. queueOwnerAWSAccountId is only valid with queueName. FIFO queues are configured on the SQS binding with fifo options, not on the provider-neutral queue definition.
The SQS transport exposes the bound queue definitions, so createRunlane() can reject missing provider bindings or queue-policy drift before the first wakeup publish. SQS still carries wakeups only: standard and FIFO settings affect provider delivery fields, not Runlane's durable run truth, lease ownership, retry policy, schedule policy, or task execution semantics.
See Workers for storage-polling worker filters and SQS Transport for SQS publish, FIFO, Lambda, and long-running consumer behavior.
What Queues Are Not
A queue is not durable truth for run state. A delayed, duplicated, or missing transport message must not decide whether a run exists or what status it has. Workers always load the current run from storage before execution.