How to move n8n off a single process and onto a horizontally scalable, Redis-backed execution fleet.
A default self-hosted n8n runs everything inside one Node.js process: the editor UI, the REST API, trigger polling, and the actual workflow executions. That is fine for a handful of automations. It stops being fine the moment a burst of webhook calls arrives, a few workflows start doing heavy data work, or you need the instance to survive a restart without dropping in-flight jobs. n8n's answer is queue mode, and this article walks through what it actually changes, how to configure it, and how to reason about concurrency once you get there.
What queue mode changes
In the default (regular) mode, the main process executes workflows itself. Under load, a single event loop competes between serving the UI and running your automations, and a crash takes running executions down with it. Queue mode separates these responsibilities. The main instance keeps the UI, the REST API, and trigger/schedule handling. Actual workflow executions are pushed onto a Redis-backed Bull queue and picked up by one or more separate worker processes. Because workers are independent processes, you scale throughput by adding more of them, and a worker crash no longer takes the editor with it.
Queue mode requires PostgreSQL and Redis. SQLite is not supported for queue-mode deployments — all instances share one database and one Redis broker, so plan those as managed, highly available services before you cut over.The three process roles
A queue-mode deployment is built from the same n8n image started with different subcommands. Conceptually there are three roles:
- Main — serves the editor UI and public API, owns trigger and schedule activation, and enqueues executions. You run at least one; you can run several in a multi-main setup for high availability.
- Worker — pulls jobs off the Redis queue and executes the workflows. This is the role you scale horizontally. Start it with the worker subcommand.
- Webhook processor (optional) — a dedicated process that only receives inbound webhook HTTP requests and enqueues them, so a flood of webhooks never competes with the editor. Start it with the webhook subcommand.
Turning on queue mode
Every instance in the deployment — main, workers, and webhook processors — must set the same execution mode and point at the same Redis. The single switch is the EXECUTIONS_MODE environment variable:
# Set on ALL n8n instances (main, workers, webhook processors)
EXECUTIONS_MODE=queue
# Redis / Bull connection (shared by every instance)
QUEUE_BULL_REDIS_HOST=redis.internal
QUEUE_BULL_REDIS_PORT=6379
QUEUE_BULL_REDIS_PASSWORD=your-redis-password
QUEUE_BULL_REDIS_DB=0
QUEUE_BULL_REDIS_TIMEOUT_THRESHOLD=10000
# Credentials are encrypted at rest; every instance needs the SAME key
# or workers cannot decrypt the credentials attached to a workflow.
N8N_ENCRYPTION_KEY=copy-the-exact-same-key-everywhereA mismatched N8N_ENCRYPTION_KEY is the single most common queue-mode failure: the main instance enqueues a job, the worker picks it up, and it fails because it cannot decrypt the credential. Copy the key verbatim to every process.Starting workers
A worker is the same n8n binary started with the worker subcommand. With the Docker image you pass worker as the command:
# Docker: start a worker
docker run --name n8n-worker \
-e EXECUTIONS_MODE=queue \
-e QUEUE_BULL_REDIS_HOST=redis.internal \
-e N8N_ENCRYPTION_KEY=... \
docker.n8n.io/n8nio/n8n worker --concurrency=10
# From a source / npm install
./packages/cli/bin/n8n worker --concurrency=10You scale throughput by running more worker containers, more concurrency per worker, or both. There is no cluster coordination to configure — every worker simply competes for jobs on the same Redis queue, and Bull hands each job to exactly one worker.
Understanding concurrency
The --concurrency flag sets how many workflow executions a single worker runs in parallel. It defaults to 10. Total cluster throughput is roughly the number of workers multiplied by their concurrency, but higher is not automatically better: every concurrent execution holds a database connection and consumes memory and CPU.
n8n recommends a concurrency of 5 or higher per worker. Very low concurrency spread across many workers can exhaust your PostgreSQL connection pool, which shows up as stalled executions rather than errors — size the DB pool for (workers x concurrency).There is a second, related knob. N8N_CONCURRENCY_PRODUCTION_LIMIT also caps production executions. In queue mode, if this variable is set to any value other than -1, n8n uses it in preference to the --concurrency flag; otherwise it falls back to --concurrency or its default of 10. Treat -1 as "unset / use the flag."
| Setting | Scope | Default | Notes |
|---|---|---|---|
| --concurrency | Per worker process | 10 | Parallel executions this worker runs at once. |
| N8N_CONCURRENCY_PRODUCTION_LIMIT | Per instance | -1 (unset) | If not -1, overrides the flag in queue mode. |
| EXECUTIONS_MODE | Every instance | regular | Must be queue on all processes. |
Dedicated webhook processors
By default the main instance still receives inbound webhook requests and enqueues them. If you receive a high volume of webhooks, move that work to dedicated webhook processors so the main process is never blocked by request handling. Start one or more processes with the webhook subcommand and put them behind your load balancer for the webhook paths:
# Dedicated webhook processor (own port, same queue mode + Redis)
docker run --name n8n-webhook -p 5679:5678 \
-e EXECUTIONS_MODE=queue \
-e QUEUE_BULL_REDIS_HOST=redis.internal \
docker.n8n.io/n8nio/n8n webhook
# On the MAIN instance, stop it from also serving production webhooks:
N8N_DISABLE_PRODUCTION_MAIN_PROCESS=trueSetting N8N_DISABLE_PRODUCTION_MAIN_PROCESS=true on the main instance disables production webhook processing there, so all inbound webhooks are handled by the dedicated processors, which enqueue them for the workers.
Monitoring and graceful shutdown
Workers can expose health endpoints so your orchestrator (Kubernetes, ECS, Nomad) can route traffic and restart unhealthy processes. Enable them with QUEUE_HEALTH_CHECK_ACTIVE:
- /healthz — returns whether the worker process is up (liveness). Requires QUEUE_HEALTH_CHECK_ACTIVE=true.
- /healthz/readiness — reports whether the worker's database and Redis connections are ready (readiness).
- /metrics — exposes performance metrics for scraping (Prometheus-style) when metrics are enabled.
# Enable worker health + readiness endpoints
QUEUE_HEALTH_CHECK_ACTIVE=true
# Give in-flight jobs time to finish before a worker is killed on deploy
N8N_GRACEFUL_SHUTDOWN_TIMEOUT=30N8N_GRACEFUL_SHUTDOWN_TIMEOUT controls how long a worker is allowed to finish its executing jobs before the process terminates. Set it comfortably above your longest expected execution so rolling deploys drain workers cleanly instead of orphaning runs. Bull will re-queue jobs that a worker never acknowledged, but a graceful drain avoids duplicate or partially completed executions.
High availability with multiple mains
A single main instance is a single point of failure for the UI, the API, and — importantly — trigger and schedule activation. Queue mode supports running more than one main instance behind a load balancer so the control plane stays available across restarts and failures. Because triggers and cron schedules must fire exactly once rather than once per main, the mains coordinate leadership through Redis: one main is elected leader and owns trigger/schedule execution, while the others serve UI and API traffic and can take over if the leader disappears.
# Enable multi-main (set on every main instance)
N8N_MULTI_MAIN_SETUP_ENABLED=trueMulti-main is an Enterprise feature and only makes sense in queue mode. Do not enable it on workers or webhook processors — it applies to main instances only, and running multiple mains without it would fire your schedules and triggers multiple times.Common failure modes
Most queue-mode incidents trace back to a small set of misconfigurations. Keep this checklist handy when a fresh deployment misbehaves:
- Executions fail immediately on workers but not the main: mismatched N8N_ENCRYPTION_KEY — the worker cannot decrypt credentials.
- Executions queue but never run: workers can't reach Redis, or EXECUTIONS_MODE isn't set to queue on the workers.
- Throughput stalls under load with no errors: PostgreSQL connection pool exhausted — reduce total concurrency or raise DB max connections.
- Webhooks time out during traffic spikes: main is still handling production webhooks — add webhook processors and set N8N_DISABLE_PRODUCTION_MAIN_PROCESS=true.
- Deploys drop running executions: N8N_GRACEFUL_SHUTDOWN_TIMEOUT too low for your longest workflow.
A minimal production topology
Pulling it together, a solid starting point looks like this:
- 1 main instance (UI, API, triggers) — with N8N_DISABLE_PRODUCTION_MAIN_PROCESS=true if you run webhook processors.
- 2+ worker instances, each --concurrency=10, behind health checks.
- 1-2 webhook processors behind the load balancer for inbound webhook paths.
- Managed PostgreSQL sized for workers x concurrency connections, plus headroom.
- Managed Redis as the Bull broker, shared by all instances.
Scale in the right order: first raise per-worker concurrency until CPU or the DB pool is the bottleneck, then add worker instances. Only add webhook processors once inbound request volume — not execution time — is the limiting factor.Wrapping up
Queue mode is the dividing line between a demo n8n and a production one. The mechanics are modest — one EXECUTIONS_MODE switch, a shared Redis, a shared encryption key, and separate worker and webhook processes — but the operational payoff is large: independent scaling, crash isolation, graceful deploys, and real health signals. Start with one main and two workers, measure your database connection usage under load, and grow concurrency and worker count from there.