Run workers
Start a long-running worker or execute a bounded batch and exit.
Workers claim stored runs and execute task handlers. Create them from a process entry point, not from each request handler. The examples import an existing runtime from src/runlane.ts.
Start a long-running worker
Create src/worker.ts and deploy it as a long-running process:
import { emailQueue, runlane } from './runlane.js'
const worker = await runlane.createDeliveryWorker({
concurrency: 4,
queues: [emailQueue.name],
})
const close = () => void worker.close()
process.once('SIGTERM', close)
try {
await worker.waitUntilClosed()
} finally {
process.off('SIGTERM', close)
await worker.close()
await runlane.close()
}createDeliveryWorker() starts the runtime and the lane's delivery path. The exact path depends on the lane:
| Lane | Worker behavior |
|---|---|
| Local | Scans the runtime's in-memory storage |
| Postgres polling | Scans durable Postgres state |
| Postgres and SQS | Receives native SQS deliveries |
The queues option contains registered logical queue names. concurrency limits in-flight work in this process. A queue concurrencyLimit is the separate durable limit shared across workers.
waitUntilClosed() observes the worker without stopping it. It rejects when a worker slot fails, allowing process supervision to restart the service.
Drain currently due work
For a bounded command, create a script such as scripts/drain-runlane.ts:
import { runlane } from '../src/runlane.js'
const { runsExecuted } = await runlane.drain({ maxRuns: 100 })
await runlane.close()
process.stdout.write(`Executed ${runsExecuted} runs\n`)Use drain() with a storage-polling lane in tests, scripts, and bounded jobs. The example prints the number of attempts it executed.
It returns when:
- no work is currently due;
maxRunshas been executed; or- its abort signal is cancelled.
Future schedules, retries, and releases do not keep drain() open. Transport lanes do not expose it because one consumer cannot prove a broker is globally empty.
Storage scans use cursors to move past stale candidates. Scan-only pages do not spend the maxRuns budget.
Shut down cleanly
worker.close() stops acquisition, aborts context.signal, and waits for shutdown. It cannot forcibly stop JavaScript or undo an external side effect.
Task handlers should:
- pass
context.signalto I/O; - stop starting new work after cancellation;
- return before shutdown can fully finish;
- use external idempotency for repeatable effects.
Repeated close() calls are safe. Close the runtime after its workers and services.
Troubleshoot a queued run
Check, in order:
- The worker registered the task id.
- Its queue filter includes the run's logical queue.
- Producer and worker use the same Runlane environment.
- The lane points to the same storage and, for transport lanes, the same transport resources.
- Queue capacity is not held by live leases or reservations.
- Maintenance is moving due retries, waits, and delivery work.
Use limit concurrency for durable capacity and run maintenance for deferred work.