n8n queue mode: when one VPS needs workers
Queue mode puts n8n executions on Redis and lets workers run them. See when one VPS gains from it, then build a pinned Compose stack with Postgres and Redis.
What n8n queue mode changes
n8n queue mode splits the work of one n8n instance across several processes. The main instance still serves the editor and receives webhooks. It also fires schedule triggers. It no longer runs production executions itself. Instead it puts each one on a Redis queue, and separate worker processes take jobs off that queue and run them. On one VPS this pays off only when a single n8n process is your bottleneck. Most small instances do better with a concurrency cap in regular mode.
You turn it on with one variable, EXECUTIONS_MODE=queue, set on every n8n process. Everything else in this guide follows from that change. Workers need the same database and the same encryption key as the main instance. This is because a worker loads the workflow and its credentials from Postgres, and it must decrypt them. Queue mode does not support SQLite, so Postgres is required. Redis becomes a new service that you run and protect.
This guide starts with the decision, then builds a Compose stack with pinned versions. As of October 2026 it pins n8n 2.41.6, with Redis on 7.4.11 and Postgres on 17.11. If you do not have n8n running yet, start with a single n8n instance behind HTTPS on a VPS. Come back when you have a reason to split it.
When does one VPS benefit from n8n workers?
n8n runs on Node.js. One Node.js process runs its JavaScript on one thread, so one n8n process can keep only one CPU core fully busy with workflow logic. In regular mode the editor, the webhook listener, the triggers and every execution share that process. Queue mode gives you several processes. This means several cores can run executions at the same time, and a slow execution in a worker does not slow the editor.
Queue mode is worth it on one VPS when you see these signs:
- The n8n container sits near 100% of one core in
docker statswhile the other cores stay idle. - The editor and webhook responses get slow while heavy workflows run. Large JSON transforms and Code nodes that loop over thousands of items are typical examples.
- One large execution runs out of memory and the whole instance restarts, editor included. In queue mode only the worker restarts, and the main instance stays up.
- You have at least 2 vCPUs (virtual CPU cores) and enough free RAM for one more full n8n process.
Stay in regular mode when these are true instead:
- The VPS has 1 vCPU. Workers would share that one core with the main instance, so you pay more memory and gain no CPU.
- Your workflows spend most of their time waiting on external APIs. Waiting does not use the CPU, and one Node.js process already handles many waiting requests at once.
- Your problem is short bursts of traffic, not steady load. A concurrency cap handles bursts with no new services.
- You cannot spare the memory. Each worker is a complete n8n process with its own baseline memory, so total memory always goes up in queue mode.
This guide gives no memory figures, because the real number depends on your workflows and installed nodes. Measure it on your own VPS with docker stats --no-stream, once before the change and once after.
The regular-mode alternative: cap concurrency
If your instance falls over during bursts, try a cap before you add workers. N8N_CONCURRENCY_PRODUCTION_LIMIT limits how many production executions run at the same time. It is disabled by default. When the limit is reached, new executions wait and then run in FIFO (first in, first out) order.
services:
n8n:
environment:
N8N_CONCURRENCY_PRODUCTION_LIMIT: 10The cap only covers production executions started by webhooks and trigger nodes. It does not apply to manual executions, sub-workflow executions, error workflows or executions started from the CLI (command-line interface). To pick a value, watch memory in docker stats during a busy period. Then lower the cap until the peak fits with room to spare.
A cap protects the box. It does not make work finish faster, because the same single process still runs every execution. If the line of waiting executions keeps growing, you need more processes. That is when queue mode makes sense.
What you need before you start
- A VPS with Docker and the Compose plugin. If Compose is new to you, read the Docker Compose basics for a VPS first.
- A reverse proxy that terminates TLS (transport layer security) for your n8n hostname.
- An n8n that already uses Postgres, or a fresh start. Moving existing data from SQLite to Postgres is a separate migration, and this guide does not cover it.
- Your existing
N8N_ENCRYPTION_KEY, if you are converting an instance that already has saved credentials.
The encryption key matters most. n8n uses it to encrypt saved credentials, and a new key cannot decrypt credentials saved under the old one. If you never set the key yourself, n8n generated one and stored it in the config file inside its data volume. Print it from your current stack with:
docker compose exec n8n cat /home/node/.n8n/configCopy the encryptionKey value into the .env file in the next step. Do not use a new random key.
Create the .env file
Put the secrets in one .env file next to compose.yaml. Compose reads this file and substitutes the values into the stack. The guide to env files and secrets in Docker Compose explains the rules for quoting and precedence.
mkdir -p ~/n8n-queue && cd ~/n8n-queue
cat > .env <<'EOF'
N8N_HOST=n8n.example.com
GENERIC_TIMEZONE=Europe/Berlin
POSTGRES_USER=n8n
POSTGRES_DB=n8n
EOF
printf 'POSTGRES_PASSWORD=%s\nREDIS_PASSWORD=%s\nN8N_ENCRYPTION_KEY=%s\n' \
"$(openssl rand -hex 24)" "$(openssl rand -hex 24)" "$(openssl rand -hex 32)" >> .env
chmod 600 .envIf you are converting an existing instance, replace the generated N8N_ENCRYPTION_KEY line with your old key. Hex output has no characters that need quoting in YAML or in a shell, so these values are safe to use as they are.
The Compose file for n8n queue mode
The file below runs one main instance, one worker service, Redis and Postgres. The shared settings live in one YAML anchor, so the main instance and the workers cannot drift apart. This is how you make sure every process gets the same N8N_ENCRYPTION_KEY and the same database.
x-n8n: &n8n
image: n8nio/n8n:2.41.6
restart: unless-stopped
environment: &n8n-env
DB_TYPE: postgresdb
DB_POSTGRESDB_HOST: postgres
DB_POSTGRESDB_DATABASE: ${POSTGRES_DB}
DB_POSTGRESDB_USER: ${POSTGRES_USER}
DB_POSTGRESDB_PASSWORD: ${POSTGRES_PASSWORD}
EXECUTIONS_MODE: queue
QUEUE_BULL_REDIS_HOST: redis
QUEUE_BULL_REDIS_PORT: 6379
QUEUE_BULL_REDIS_PASSWORD: ${REDIS_PASSWORD}
QUEUE_HEALTH_CHECK_ACTIVE: "true"
N8N_ENCRYPTION_KEY: ${N8N_ENCRYPTION_KEY}
N8N_DEFAULT_BINARY_DATA_MODE: database
GENERIC_TIMEZONE: ${GENERIC_TIMEZONE}
TZ: ${GENERIC_TIMEZONE}
healthcheck:
test: ["CMD", "node", "-e", "fetch('http://127.0.0.1:5678/healthz').then(r=>process.exit(r.ok?0:1)).catch(()=>process.exit(1))"]
interval: 30s
timeout: 5s
retries: 3
start_period: 60s
services:
n8n:
<<: *n8n
environment:
<<: *n8n-env
N8N_HOST: ${N8N_HOST}
N8N_PROTOCOL: https
WEBHOOK_URL: https://${N8N_HOST}/
ports:
- "127.0.0.1:5678:5678"
volumes:
- n8n_data:/home/node/.n8n
depends_on:
postgres:
condition: service_healthy
redis:
condition: service_healthy
n8n-worker:
<<: *n8n
command: worker --concurrency=5
mem_limit: 1g
depends_on:
n8n:
condition: service_healthy
redis:
image: redis:7.4.11-alpine
restart: unless-stopped
command: redis-server --requirepass ${REDIS_PASSWORD} --appendonly yes
environment:
REDISCLI_AUTH: ${REDIS_PASSWORD}
volumes:
- redis_data:/data
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 10s
timeout: 3s
retries: 5
postgres:
image: postgres:17.11-alpine
restart: unless-stopped
environment:
POSTGRES_USER: ${POSTGRES_USER}
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
POSTGRES_DB: ${POSTGRES_DB}
volumes:
- pg_data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U $${POSTGRES_USER} -d $${POSTGRES_DB}"]
interval: 10s
timeout: 3s
retries: 5
volumes:
n8n_data:
redis_data:
pg_data:Why each setting is there
Redis has no ports: entry. The workers and the main instance reach Redis by its service name, redis, on the Compose network. A Redis port published on the host can be reached from the internet unless a firewall blocks it. On many setups Docker's published ports also bypass ufw rules. The password adds a second layer on top of that. REDISCLI_AUTH is the variable that redis-cli reads for its password. The health check and your own redis-cli commands then log in without the password on the command line.
--appendonly yes makes Redis write every change to disk, so jobs waiting in the queue survive a Redis restart. Keep the default memory policy, noeviction. An eviction policy such as allkeys-lru lets Redis delete keys when memory fills up, and it is not safe to delete queue keys.
N8N_DEFAULT_BINARY_DATA_MODE: database is needed because of how queue mode works. The default mode keeps binary data (files, images, PDFs) in memory. n8n does not support filesystem mode in queue mode, because a worker cannot read files written to the main container's disk. The database mode stores binary data in Postgres. The other supported choice is S3-compatible object storage. Large files in Postgres make the database grow, so prune old executions.
--concurrency=5 sets how many jobs one worker runs at the same time. The default is 10. n8n's docs recommend 5 or more per worker. Many workers with very low concurrency each open their own database connections, so you run out of Postgres connections before you run out of CPU. The guide to Postgres connection pooling on a VPS shows how to count connections against max_connections. Do not set N8N_CONCURRENCY_PRODUCTION_LIMIT in queue mode unless you mean to. If it is set to any value other than -1, it overrides the --concurrency flag on every worker.
mem_limit: 1g stops one worker from using all the memory on the VPS. When a worker goes past its limit, the kernel kills that container and Docker restarts it. The main instance and Postgres keep running. Treat 1g as a starting value, and set your own from docker stats readings. The guide to memory limits in Docker Compose covers how the limit and swap interact.
QUEUE_HEALTH_CHECK_ACTIVE: "true" makes each worker serve /healthz on port 5678 inside its own container. The main instance always serves /healthz. The health check calls node, which is always present in the n8n image, so it does not depend on wget or curl being installed. The depends_on conditions set the start order: Postgres and Redis first, then the main instance, then the workers. The main instance runs the database migrations on first start, so the workers wait for it. The guide to Docker Compose healthchecks explains start_period and the other timings.
Workers have no volume and no published port. They read everything they need from Postgres and Redis. Do not share the main instance's n8n_data volume with them.
Start the stack and check each part
docker compose up -d
docker compose psAfter a minute or two, docker compose ps should show all four services with (healthy) in the status column. If a service stays at (health: starting) or shows (unhealthy), its logs show the reason:
docker compose logs --tail=50 n8n
docker compose logs --tail=50 n8n-workerCheck that Redis answers, and that the n8n processes are connected to it:
docker compose exec redis redis-cli ping
docker compose exec redis redis-cli info clientsThe first command should print PONG. In the second, connected_clients should be more than 1, because the main instance and each worker hold their own connections.
Check that Redis cannot be reached from outside the Compose network:
sudo ss -tlnp | grep 6379This should print nothing. A line in the output means something on the host listens on port 6379. Find out what it is before you continue.
Now prove that the workers run your executions. Open the editor, activate a workflow with a Webhook trigger, and call its production URL with curl. The execution should show as successful in the editor's execution list. Then stop the workers and call the URL again, with your-path replaced by your webhook's path:
docker compose stop n8n-worker
curl -s --max-time 10 https://n8n.example.com/webhook/your-path
docker compose start n8n-worker--max-time 10 stops curl from waiting forever if the webhook only responds when the workflow finishes. While the workers are stopped, the new execution does not finish, because nothing is taking jobs off the queue. It runs after the workers start again. If it finishes while the workers are stopped, the main instance is not in queue mode. Check with docker compose exec n8n printenv EXECUTIONS_MODE.
Add a second worker
On one VPS, scaling is one flag:
docker compose up -d --scale n8n-worker=2
docker compose ps n8n-workerEach copy gets the same settings from the anchor. Add workers only while CPU cores and memory are free. On a 2 vCPU VPS, two workers already compete with the main instance and Postgres for CPU time. A third worker there usually adds memory use and no throughput. After scaling, check the Postgres connections:
docker compose exec postgres psql -U n8n -d n8n -c "select count(*) from pg_stat_activity where datname = 'n8n';"The count should stay well below the server's max_connections. Postgres sets that to 100 by default, and the official image keeps the default.
What queue mode does not fix
The main instance is still one process. It serves the editor and receives every webhook, and it fires every schedule trigger. If it crashes, new webhooks fail and scheduled workflows do not start, even while the workers are healthy. The single-process failures in why a self-hosted n8n keeps going offline still apply to the main instance. Queue mode takes execution load off that process. It does not make the process itself more reliable.
Manual executions, the ones you start from the editor, still run on the main instance by default. If testing a heavy workflow in the editor is what knocks your instance over, set OFFLOAD_MANUAL_EXECUTIONS_TO_WORKERS=true on the main instance.
Code nodes run in task runners. In n8n 2.x the default internal mode starts them as child processes inside each n8n container, so this stack works without extra services. For production, n8n's docs recommend external mode, with one runner sidecar container per worker. The reason is that internal mode does not isolate code that escapes the sandbox. Plan for that before you run Code nodes you do not trust.
Running several main instances for high availability is a separate feature called multi-main. It needs a paid license, as explained in the comparison of n8n Community and Enterprise. Queue mode with workers is available in the free Community edition.
Upgrades and backups
Every n8n process must run the same version, because they all share one database schema. Change the tag in the anchor, then recreate all the services together:
docker compose pull
docker compose up -dBack up Postgres before every n8n upgrade. The main instance migrates the schema when it starts, and going back to the old image will not undo that. Postgres holds your workflows, credentials, executions and, in this setup, binary data. The .env file holds the encryption key, and a database backup without that key cannot decrypt your credentials. The guide to backing up and upgrading a Docker Compose stack has a full routine. Redis does not need a long-term backup, since it only holds jobs that are in progress.
FAQ
When should I use n8n queue mode on a single VPS?
Use it when one n8n process is the bottleneck and the VPS has spare cores and memory. Typical signs: the n8n container sits near 100% of one core while the other cores are idle, or the editor gets slow while heavy workflows run. On a 1 vCPU VPS, or when your workflows mostly wait on external APIs, stay in regular mode and set N8N_CONCURRENCY_PRODUCTION_LIMIT instead.
Do n8n workers need the same encryption key as the main instance?
Yes. Workers load credentials from Postgres and decrypt them with N8N_ENCRYPTION_KEY. A worker with a different key cannot decrypt credentials saved by the main instance, so any execution that uses those credentials fails. Set the key once in .env and pass it to every n8n process.
Should Redis be published on a host port for n8n queue mode?
No. The main instance and the workers reach Redis by its service name on the Compose network, so the Redis service needs no ports: entry. A published port can be reachable from the internet, and on many setups Docker's published ports bypass ufw rules. Keep Redis unpublished and set a password with --requirepass.
Can I keep SQLite or filesystem binary data in queue mode?
No to both. n8n queue mode requires Postgres. It also does not support the filesystem binary data mode, because workers cannot read files on the main container's disk. Set N8N_DEFAULT_BINARY_DATA_MODE to database, or use S3-compatible storage.
How many workers and how much concurrency should I use?
Start with one worker at --concurrency=5, which follows n8n's advice of 5 or more per worker. Add a second worker only if CPU cores and memory are free. Check Postgres connections in pg_stat_activity after each change. Do not set N8N_CONCURRENCY_PRODUCTION_LIMIT in queue mode unless you want it to override the worker flag.