Execution Models
How the Node worker runs jobs — the event loop, concurrency, and CPU-bound work.
How the Node worker runs jobs — the event loop, concurrency, and CPU-bound work.
The Rust core owns scheduling — it claims due jobs from storage and dispatches them. Execution happens back in your Node process: the core hands each job to a JavaScript callback and awaits the returned promise. Handlers run on the single Node event loop, just like the rest of your app.
Jobs run concurrently, not one at a time. Each dispatched job is an independent async invocation, so while one handler awaits I/O another can run. In-flight work is bounded by:
concurrency — how many attempts this worker runs at once. Unset, it is
unbounded; set it on any worker that shares a database. It counts attempts,
not live handlers: a timed-out attempt
frees its slot while its handler may still be running.maxConcurrent and queue concurrency — explicit
caps across every worker.channelCapacity (default 128) is the worker's dispatch buffer, not a cap on
running jobs. batchSize (default 1) controls how many jobs the scheduler
claims per poll — raise it to amortize polling under high throughput.
queue.runWorker({ queues: ["default"], concurrency: 32, batchSize: 16 });There is no GIL, thread-pool sizing, or prefork choice to make — concurrency is ordinary event-loop async.
Because handlers share the event loop, a long synchronous computation blocks every in-flight job. For CPU-heavy tasks:
worker_threads
pool or a native addon, and await it.Horizontal scaling is just more worker processes (or machines) pointed at shared Postgres or Redis storage. The core claims each job for a single worker, so adding workers adds throughput without duplicate execution — see guarantees.
Async handlers are first-class: return a promise and the worker awaits it. A sync handler works too — it just can't yield the loop, so keep it short.