How to Scale n8n with Redis Queue Mode for Parallel Workflow Execution
You know the moment. Your n8n instance is humming along, running 20 workflows, and then someone wires up a webhook that suddenly receives a few hundred events per minute. Everything starts backing up. Executions that used to finish in seconds now take minutes. A sync job blocks an API call, which blocks a webhook, which times out, which triggers a retry storm.
That's the single-process ceiling, and no amount of CPU will save you from it. What you need is queue mode.
This is the same production pattern Elestio uses internally and the one recommended by the n8n team for anything beyond a handful of concurrent executions. It's not complicated, but the official docs scatter the relevant bits across five pages. Let's put it all in one place.
What queue mode actually does
By default, n8n runs in what's called regular mode: one Node.js process handles the UI, the webhooks, and every workflow execution. It's fine for 1–10 executions at a time. Past that, you're gambling.
Queue mode splits the single process into three roles:
- Main process: serves the UI, receives webhooks, schedules executions
- Workers: pull jobs off a Redis queue and run them in their own processes
- Webhook processors (optional): dedicated processes that only accept incoming webhooks
Redis sits in the middle as the job broker. When a webhook fires or a cron trigger fires, the main process drops a job in Redis. Any available worker picks it up. You can have 1 worker or 50, and n8n will happily parallelize across all of them.
The result: webhooks stop timing out because they're no longer competing with executions for CPU. And executions run in parallel instead of stacking up in a single event loop.
When you actually need this
Don't turn on queue mode because a blog post told you to. Turn it on when you hit one of these:
- Webhook latency climbing above 1 second. If your webhook response time is creeping up during busy periods, your main loop is blocked by executions.
- More than 10–20 concurrent executions. Regular mode will technically run them, but you'll see event-loop lag.
- Long-running workflows blocking short ones. A 5-minute data sync shouldn't delay a 200ms Slack notification.
- You want zero-downtime deployments. Queue mode lets you roll workers one at a time without dropping in-flight jobs.
If none of those apply, stay on regular mode. It's simpler, cheaper, and one less moving part.
The architecture
Here's the minimal queue-mode setup:
| Component | Role | Typical count |
|---|---|---|
| n8n main | UI, webhooks, scheduler, enqueues jobs | 1 |
| n8n worker | Pulls jobs from Redis, executes workflows | 2–10+ |
| Redis | Job queue and locks (via BullMQ) | 1 (can be clustered) |
| PostgreSQL | Shared execution data, credentials, workflows | 1 |
The main process and workers all read and write to the same Postgres. Redis is only used for the queue and some ephemeral locks, not for execution data.
Setting it up with Docker Compose
Here's a production-ready docker-compose.yml that boots a main instance plus two workers:
services:
postgres:
image: postgres:16
restart: unless-stopped
environment:
POSTGRES_USER: n8n
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
POSTGRES_DB: n8n
volumes:
- postgres_data:/var/lib/postgresql/data
redis:
image: redis:7-alpine
restart: unless-stopped
command: redis-server --requirepass ${REDIS_PASSWORD}
volumes:
- redis_data:/data
n8n-main:
image: n8nio/n8n:latest
restart: unless-stopped
ports:
- "5678:5678"
environment:
EXECUTIONS_MODE: queue
DB_TYPE: postgresdb
DB_POSTGRESDB_HOST: postgres
DB_POSTGRESDB_DATABASE: n8n
DB_POSTGRESDB_USER: n8n
DB_POSTGRESDB_PASSWORD: ${POSTGRES_PASSWORD}
QUEUE_BULL_REDIS_HOST: redis
QUEUE_BULL_REDIS_PASSWORD: ${REDIS_PASSWORD}
N8N_ENCRYPTION_KEY: ${N8N_ENCRYPTION_KEY}
N8N_HOST: ${N8N_HOST}
WEBHOOK_URL: https://${N8N_HOST}
depends_on:
- postgres
- redis
n8n-worker:
image: n8nio/n8n:latest
restart: unless-stopped
command: worker --concurrency=10
environment:
EXECUTIONS_MODE: queue
DB_TYPE: postgresdb
DB_POSTGRESDB_HOST: postgres
DB_POSTGRESDB_DATABASE: n8n
DB_POSTGRESDB_USER: n8n
DB_POSTGRESDB_PASSWORD: ${POSTGRES_PASSWORD}
QUEUE_BULL_REDIS_HOST: redis
QUEUE_BULL_REDIS_PASSWORD: ${REDIS_PASSWORD}
N8N_ENCRYPTION_KEY: ${N8N_ENCRYPTION_KEY}
depends_on:
- postgres
- redis
deploy:
replicas: 2
volumes:
postgres_data:
redis_data:
A few things worth flagging:
EXECUTIONS_MODE: queueis the switch. Without it, the main process will still run executions locally and ignore the queue entirely.N8N_ENCRYPTION_KEYmust be identical across main and all workers. Different keys = workers can't decrypt credentials = every workflow fails silently. I've seen this one take down a production instance for two hours.--concurrency=10tells each worker how many jobs to run in parallel. Start at 10 and tune based on your workload.- Workers don't expose any ports. They only talk to Redis and Postgres.
Tuning worker concurrency
The right concurrency number depends on what your workflows actually do:
- I/O-heavy workflows (HTTP calls, database queries, webhooks): set concurrency to 10–20 per worker. Most of the time is spent waiting.
- CPU-heavy workflows (image processing, large data transformations, custom JS): keep concurrency at 2–5 per worker, and add more workers instead.
- Mixed workloads: start at 10 and watch CPU. If workers pin at 100% CPU, reduce concurrency. If they sit at 20% with a growing queue, increase it.
The formula most teams land on: workers × concurrency ≈ 2× expected peak concurrent executions. Gives you headroom without overprovisioning.
Troubleshooting
Workers aren't picking up jobs. Check that EXECUTIONS_MODE: queue is set on both the main process and the workers. If only the main has it, executions run locally and the queue stays empty.
Workflows fail with "credential decryption error". Your N8N_ENCRYPTION_KEY differs between main and workers. Must be byte-identical.
Executions get stuck in "running" state forever. Usually a worker crashed mid-execution. Enable Redis persistence (appendonly yes) and use graceful shutdown by setting N8N_GRACEFUL_SHUTDOWN_TIMEOUT=30 on workers.
High Redis memory use. BullMQ keeps completed job metadata by default. Set QUEUE_BULL_JOB_OPTIONS_REMOVE_ON_COMPLETE=1000 to keep only the last 1,000.
Webhook responses are slow even with queue mode. If you're using synchronous webhook responses ("respond when workflow completes"), the main process still waits. For high-throughput webhooks, use "respond immediately" and let the workflow finish async.
Deploy this on Elestio
Running queue-mode n8n means managing Postgres, Redis, the main instance, and workers, plus TLS, backups, and updates. Elestio's managed n8n service handles the infrastructure side: managed Postgres, managed Redis, automated daily backups, TLS, and one-click updates. You pick a VM size, deploy, and customize the Docker Compose to add workers when you need them.
If you prefer to self-manage, the Docker Compose above runs cleanly on any VM with 4 GB of RAM minimum (8 GB+ recommended once you have more than 2 workers).
Thanks for reading ❤️ See you in the next one 👋