← Back to Blog

How to Run n8n in Queue Mode with Workers on Two VPS: High Availability Without the Enterprise Licence

Published · by Leo Gashi, RS Computers

n8n Queue mode High availability

n8n high availability in the free Community edition means one main process plus as many workers as you want, all talking through Redis and PostgreSQL, with the main set up to come back within seconds when it dies. That is queue mode, and it needs no licence; two mains standing in for each other (n8n calls it multi-main) is an Enterprise feature listed as "Contact Sales". We built the Community version on a 2 vCPU, 3 GiB test machine on 28 October 2026 with n8n 2.45.5, then killed a worker mid-burst, restarted the main mid-burst and stopped Redis. A dead worker cost 10 of 200 executions and was back in a second; a main restart took it offline for about 14 seconds, in which 99 of 400 webhook calls aimed at it failed, while calls aimed at a webhook processor all succeeded.

This is the scaling chapter for people who already run n8n on one server; if you are starting from zero, read how to self-host n8n with Docker first, and see our n8n VPS requirements load test for single-instance numbers.

Key facts, checked on 28 October 2026:

How n8n high availability works in the Community edition, and what it cannot do

A normal n8n install is one process that shows the editor, listens for webhooks, fires timers and runs the workflows. Queue mode splits that into roles. The main keeps the editor, the timers and pollers (triggers that ask a service every minute for new data) and, by default, the webhook listener; it creates an execution record, puts its ID on a queue in Redis (an in-memory store) and runs nothing itself. Workers take IDs from Redis, load the workflow from PostgreSQL, run it and write the result; you can run any number on any server that reaches both. Optional webhook processors only accept webhook calls and queue them, so a flood never touches the main.

Here is the honest part. Everything except the main can be duplicated for free. The main cannot: while it is down the editor is down, timers do not fire and webhooks pointed at it fail. Several mains with an elected leader is multi-main, which needs an Enterprise licence and a load balancer with sticky sessions (n8n docs). So self-hosted n8n HA without a licence is four things done well: a main that restarts within seconds and is watched from outside; webhooks received by a processor so a main restart loses none; workers on two servers; and Postgres and Redis sized, backed up and never reachable from the internet. That covers the failures you will actually see, not the main server dying at 3 a.m. with nobody awake.

What we broke on purpose, and what happened

Everything ran inside one Debian 13 machine limited to 2 vCPU and 3 GiB: Docker Compose, the images n8nio/n8n:2.45.5, postgres:17 and redis:7-alpine, two workers at concurrency 10, and a three-node workflow (Webhook responding immediately, Wait 1 second, Edit Fields). A burst is 200 GET requests, 20 at a time.

TestWhat happenedWhat it means
200 calls at the mainAll answered 200 in 9.4 s (median 0.76 s, slowest 2.26 s). Worker 1 ran 103, worker 2 ran 97. All succeeded.Redis spreads work evenly with no setting. The main used 45% of a core just accepting webhooks.
200 calls at the webhook processorAll answered 200. Main CPU 2%, processor 52% of a core.A processor takes the whole incoming load off the main.
Worker killed with SIGKILL 3 s into a burstDocker restarted it within a second. 190 of 200 succeeded; the dead worker had started 11 and finished 3. Two were marked "crashed" at once, eight more within five minutes. None re-ran.A dead worker loses what it had in hand.
docker compose restart n8n-main, calls at the processorAll 200 answered and succeeded. The main was unreachable for about 14 s and peaked at 608 MiB while starting.With a processor, a main restart is invisible to senders. Only timers and the editor pause.
Same restart, 400 calls at the main301 answered 200, 99 got a connection error.Without a processor every main restart drops webhooks for the whole window.
Redis stopped for 25 sA webhook call hung 13.9 s, then failed. Every n8n process logged "Exiting process due to Redis connection error" after 10 s; Docker restarted them in a loop, and 40 s after Redis returned all were healthy.Redis takes the whole system down. It used 16 MiB, but it must be watched.
docker kill on a workerStayed "Exited (137)": Docker treats kill and stop as manual and ignores the restart policy (Docker docs).Test crashes by killing the process inside the container.

After 1,905 executions the Postgres database was 22 MB, and the three images take 2.5 GB of disk. Memory figures are in the plan section below.

Step 1: one Compose file for Postgres, Redis, main, workers and a webhook processor

You need Docker Engine with the Compose plugin and a reverse proxy with HTTPS; the n8n setup guide covers both. Below is the file we ran (the lab had no domain, so there N8N_HOST was 127.0.0.1:5678 over http). Secrets go in a .env file. The encryption key is the one line you must never lose or change: every credential in Postgres is encrypted with it, and every worker and processor needs the same key to read them (n8n docs).

# as root on server A
mkdir -p /opt/n8n && cd /opt/n8n
printf "POSTGRES_PASSWORD=%s\nREDIS_PASSWORD=%s\nN8N_ENCRYPTION_KEY=%s\nN8N_HOST=n8n.example.com\nN8N_PROTOCOL=https\n" \
  $(openssl rand -hex 24) $(openssl rand -hex 24) $(openssl rand -hex 32) > .env
chmod 600 .env && cat .env
POSTGRES_PASSWORD=2cf2b189...
REDIS_PASSWORD=e95f4e59...
N8N_ENCRYPTION_KEY=4149b1d3...
N8N_HOST=n8n.example.com
N8N_PROTOCOL=https

Copy the key somewhere safe off the server now. The x-n8n-env block is written once and merged into main, workers and processor, so they can never disagree about the database or the key:

# as root on server A: /opt/n8n/compose.yaml
x-n8n-env: &n8n-env
  EXECUTIONS_MODE: queue
  QUEUE_BULL_REDIS_HOST: redis
  QUEUE_BULL_REDIS_PORT: "6379"
  QUEUE_BULL_REDIS_PASSWORD: ${REDIS_PASSWORD}
  QUEUE_HEALTH_CHECK_ACTIVE: "true"
  DB_TYPE: postgresdb
  DB_POSTGRESDB_HOST: postgres
  DB_POSTGRESDB_PORT: "5432"
  DB_POSTGRESDB_DATABASE: n8n
  DB_POSTGRESDB_USER: n8n
  DB_POSTGRESDB_PASSWORD: ${POSTGRES_PASSWORD}
  N8N_ENCRYPTION_KEY: ${N8N_ENCRYPTION_KEY}
  N8N_HOST: ${N8N_HOST}
  N8N_PROTOCOL: ${N8N_PROTOCOL}
  N8N_EDITOR_BASE_URL: ${N8N_PROTOCOL}://${N8N_HOST}/
  N8N_WEBHOOK_URL: ${N8N_PROTOCOL}://${N8N_HOST}/
  N8N_PROXY_HOPS: "1"
  GENERIC_TIMEZONE: Europe/Amsterdam
  TZ: Europe/Amsterdam
  N8N_DIAGNOSTICS_ENABLED: "false"
  N8N_GRACEFUL_SHUTDOWN_TIMEOUT: "60"
  OFFLOAD_MANUAL_EXECUTIONS_TO_WORKERS: "true"
  N8N_ENFORCE_SETTINGS_FILE_PERMISSIONS: "true"
  EXECUTIONS_DATA_PRUNE: "true"
  EXECUTIONS_DATA_MAX_AGE: "168"

services:
  postgres:
    image: postgres:17
    restart: always
    environment:
      POSTGRES_USER: n8n
      POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
      POSTGRES_DB: n8n
    volumes:
      - pg_data:/var/lib/postgresql/data
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U n8n -d n8n"]
      interval: 5s
      timeout: 5s
      retries: 10

  redis:
    image: redis:7-alpine
    restart: always
    command: ["redis-server", "--requirepass", "${REDIS_PASSWORD}", "--maxmemory-policy", "noeviction", "--appendonly", "yes"]
    environment:
      REDIS_PASSWORD: ${REDIS_PASSWORD}
    volumes:
      - redis_data:/data
    healthcheck:
      test: ["CMD-SHELL", "redis-cli -a $$REDIS_PASSWORD ping | grep -q PONG"]
      interval: 5s
      timeout: 5s
      retries: 10

  n8n-main:
    image: n8nio/n8n:2.45.5
    restart: always
    environment:
      <<: *n8n-env
    ports:
      - "127.0.0.1:5678:5678"
    volumes:
      - n8n_main:/home/node/.n8n
    depends_on:
      postgres:
        condition: service_healthy
      redis:
        condition: service_healthy
    healthcheck:
      test: ["CMD-SHELL", "wget -qO- http://127.0.0.1:5678/healthz/readiness >/dev/null || exit 1"]
      interval: 10s
      timeout: 5s
      retries: 6
      start_period: 60s

  n8n-worker:
    image: n8nio/n8n:2.45.5
    restart: always
    command: ["worker", "--concurrency=10"]
    environment:
      <<: *n8n-env
    depends_on:
      postgres:
        condition: service_healthy
      redis:
        condition: service_healthy
    healthcheck:
      test: ["CMD-SHELL", "wget -qO- http://127.0.0.1:5678/healthz/readiness >/dev/null || exit 1"]
      interval: 10s
      timeout: 5s
      retries: 6
      start_period: 60s

  n8n-webhook:
    image: n8nio/n8n:2.45.5
    restart: always
    command: ["webhook"]
    environment:
      <<: *n8n-env
    ports:
      - "127.0.0.1:5680:5678"
    depends_on:
      postgres:
        condition: service_healthy
      redis:
        condition: service_healthy
    healthcheck:
      test: ["CMD-SHELL", "wget -qO- http://127.0.0.1:5678/healthz/readiness >/dev/null || exit 1"]
      interval: 10s
      timeout: 5s
      retries: 6
      start_period: 60s

volumes:
  pg_data:
  redis_data:
  n8n_main:

The lines that are not obvious:

Two things we left out: binary data stays in the database in queue mode, because filesystem mode is not supported there and S3 needs a paid licence (binary data docs); and the built-in Code node runner is logged as deprecated in favour of a separate n8nio/runners container per process (task runner docs), which we did not test.

Step 2: start it, make a test workflow, fire 200 webhooks

# as root on server A
cd /opt/n8n && docker compose up -d --scale n8n-worker=2
sleep 60 && docker compose ps --format "table {{.Name}}\t{{.Service}}\t{{.Status}}"
NAME                SERVICE       STATUS
n8n-n8n-main-1      n8n-main      Up About a minute (healthy)
n8n-n8n-webhook-1   n8n-webhook   Up About a minute (healthy)
n8n-n8n-worker-1    n8n-worker    Up About a minute (healthy)
n8n-n8n-worker-2    n8n-worker    Up About a minute (healthy)
n8n-postgres-1      postgres      Up About a minute (healthy)
n8n-redis-1         redis         Up About a minute (healthy)

--scale n8n-worker=2 starts two workers; change the number and rerun any time. The worker log confirms queue mode:

docker compose logs n8n-worker --no-log-prefix | grep -E "Version|Concurrency|Queue"
 * Version: 2.45.5
 * Concurrency: 10
 * Queue: jobs

Open the editor through your proxy, create the owner, and build the three nodes (Webhook trigger with path burst, method GET, "Respond: Immediately"; Wait 1 second; Edit Fields). Activate it, then fire the burst with your domain:

# as any user on server A
time (seq 200 | xargs -P 20 -I{} curl -s -o /dev/null -m 10 -w "%{http_code}\n" \
  https://n8n.example.com/webhook/burst | sort | uniq -c)
sleep 25
docker compose logs --since 2m n8n-worker | grep "finished execution" | awk '{print $1}' | sort | uniq -c
    200 200
real    0m9.431s
    103 n8n-worker-1
     97 n8n-worker-2

If the webhook answers but nothing runs, a worker is missing EXECUTIONS_MODE, has a different encryption key or cannot reach Redis; its log says which.

Step 3: make the single main process restart fast and be watched

Docker does the restart. restart: always brought the killed worker back within a second and every n8n process back after the Redis outage. Docker counts a start as successful after 10 seconds and leaves a container that dies faster alone (Docker docs); do not add systemd units on top.

A webhook processor keeps accepting during the restart. Route /webhook/* and /webhook-waiting/* in your reverse proxy to port 5680 and everything else, including /webhook-test/*, to 5678 (n8n docs), then set N8N_DISABLE_PRODUCTION_MAIN_PROCESS: "true" on the main. Many webhook senders retry on a connection error, but not all; check the ones you depend on, and put risky steps behind an Error Workflow, because executions a dying worker had in hand end as crashed.

Watch from another server. Run an Uptime Kuma monitor on https://n8n.example.com/healthz and on the webhook path from a different machine, with Telegram alerts; our Uptime Kuma and Grafana guide shows how. /healthz/readiness also confirms Postgres is reachable (n8n monitoring docs). When upgrading, change the pinned tag and restart main before the workers.

Step 4: add a worker on a second VPS over WireGuard

One kernel panic still stops a single server, so the Community step up is a second server that runs only workers. Workers need Redis on 6379 and Postgres on 5432 from server A, and neither may ever be reachable from the internet, so the servers talk over a tunnel. Our WireGuard VPN server page covers the install; you want server A at 10.8.0.1, server B at 10.8.0.2, and ping 10.8.0.1 working from B. Then publish Redis and Postgres on that address only:

# as root on server A
cd /opt/n8n && echo "PRIVATE_IP=10.8.0.1" >> .env
# in compose.yaml, under redis:      ports: ["${PRIVATE_IP}:6379:6379"]
# in compose.yaml, under postgres:   ports: ["${PRIVATE_IP}:5432:5432"]
docker compose config -q && docker compose up -d
ss -tlnp | grep -E "6379|5432|5678|5680"
LISTEN 0 4096   127.0.0.1:5680   users:(("docker-proxy",...))
LISTEN 0 4096   127.0.0.1:5678   users:(("docker-proxy",...))
LISTEN 0 4096  10.8.0.1:6379     users:(("docker-proxy",...))
LISTEN 0 4096  10.8.0.1:5432     users:(("docker-proxy",...))

This is the most important firewall line in the setup. Docker's published ports skip ufw, so a plain ports: ["6379:6379"] puts Redis on the public internet whatever ufw says; binding to the WireGuard address means the port exists only on the tunnel. On the lab machine, with its private address standing in for 10.8.0.1, redis-cli -h 10.8.0.1 ping without a password got NOAUTH Authentication required. and redis-cli -h 127.0.0.1 ping got Connection refused.

Server B gets Docker, the same n8n version and a Compose file with only the worker: the x-n8n-env block from server A with QUEUE_BULL_REDIS_HOST and DB_POSTGRESDB_HOST changed to 10.8.0.1, plus a copy of .env, because the encryption key must be identical.

# as root on server B: /opt/n8n-worker/compose.yaml (x-n8n-env block above, two hosts changed)
services:
  n8n-worker:
    image: n8nio/n8n:2.45.5
    restart: always
    command: ["worker", "--concurrency=10"]
    environment:
      <<: *n8n-env
    healthcheck:
      test: ["CMD-SHELL", "wget -qO- http://127.0.0.1:5678/healthz/readiness >/dev/null || exit 1"]
      interval: 10s
      timeout: 5s
      retries: 6
      start_period: 60s
# as root on server B
cd /opt/n8n-worker && chmod 600 .env && docker compose up -d && sleep 50
docker compose ps --format "{{.Name}} {{.Status}}"
docker compose logs --no-log-prefix | grep -E "Version|Concurrency|Registered"
n8n-worker-n8n-worker-1 Up 50 seconds (healthy)
 * Version: 2.45.5
 * Concurrency: 10
Registered runner "JS Task Runner" (9diag9WVfL5qPgmaToQWB)

We ran exactly this worker as a second Compose project against Redis and Postgres on the private address, then fired 200 webhooks at the main: the local worker ran 103, the "second server" worker 97, all succeeded. The WireGuard tunnel itself did not fit in one 3 GiB lab machine, so confirm the 10.8.0.x addresses with ping first. Keep both servers in the same city: every execution makes several round trips to Postgres and Redis.

Redis, Postgres and backups: the parts that must not fail

Every n8n process gives up after 10 seconds without Redis (QUEUE_BULL_REDIS_TIMEOUT_THRESHOLD) and exits; Docker restarts them until Redis is back, but for the whole outage no webhook is accepted and no timer fires. So Redis and Postgres belong on the server with dedicated CPU and the most RAM. Tuning is in our PostgreSQL, MySQL and Redis on a VPS guide, sizing in how to size a VDS for a production database. Postgres holds the real state; dump it daily and get the dump off the server:

# as root on server A, from cron, daily
cd /opt/n8n && docker compose exec -T postgres pg_dump -U n8n -d n8n -Fc > /var/backups/n8n-$(date +%F).dump

Push the dumps and the .env file elsewhere with restic, as in our off-site backups with restic and MinIO guide; a dump without the encryption key is a list of credentials you can never open. Our servers in Prishtina include weekly backups of the whole server with one-click restore from the client area (a daily add-on exists); Amsterdam and Dublin servers do not include backups, so there the restic job is not optional.

Which RS Computers plan for which n8n layout

Measured at idle: 358 MiB for the main (608 MiB while starting), 254 MiB for the processor, 306 to 308 MiB per worker, 57 MiB for Postgres, 16 MiB for Redis; 1,361 MB used on the whole machine. Under a burst the workers peaked near 450 MiB each and the main at 410 MiB. Workers also burn the CPU, 80 to 95% of a core each in our burst, so the vCPU type matters most for them: VPS plans have shared vCPUs, VDS plans dedicated vCPUs.

SituationLayoutPlanCity
First queue-mode setup, a few thousand executions a day, light Code nodesAll on one server: main, processor, 2 workers, Postgres, RedisVPS Mini: 4 vCPU (shared), 4 GB RAM, 80 GB NVMe. About 1.4 GB idle, 2 GB in burstsAmsterdam, Dublin or Prishtina; Prishtina includes weekly backups
Production for a business, bursts of webhooks, steady CPU wantedAll on one server, 2 to 4 workersVDS Small: 4 vCPU (dedicated), 8 GB RAM, 240 GB NVMe, 10 Gb/sThe city nearest your users and APIs
A dead server should cost capacity, not everythingA: main, processor, Postgres, Redis. B: 2 to 3 workers over WireGuardA: VDS Small. B: VPS Mini for light workflows, a second VDS Small for heavy Code or AI nodesBoth servers in the same city
Heavy Code nodes, AI agents, big files, 5 or more workersOne big server, or A as above and a bigger BVDS Medium: 8 vCPU (dedicated), 16 GB RAM, 480 GB NVMeSame city for A and B
Enterprise licence with multi-mainTwo mains behind a load balancer with sticky sessions, workers, a separate database serverTwo VDS Small for the mains, VDS Medium for Postgres and Redis, workers on VPS Mini or VDS SmallSame city

Every plan has NVMe, unmetered traffic and its own IPv4 and IPv6 address, and an upgrade from the client area is a short reboot, so starting on a VPS Mini and moving up when the worker CPU graph stays high is normal. Plans and install options are on the n8n VPS hosting page.

When you do need Enterprise multi-main

Multi-main means several mains, a leader that runs timers and pruning, and followers that take over when the leader misses its key TTL (10 seconds by default, checked every 3). It needs every main in queue mode on the same Postgres and Redis, the same n8n version everywhere, N8N_MULTI_MAIN_SETUP_ENABLED=true, sticky sessions and a licence (n8n docs). On the pricing page (read 28 October 2026) the self-hosted Business plan at 667 € a month billed annually does not list it; Enterprise is "Contact Sales". We did not test it. You need it when a 15-second gap in timers or the editor is a real problem, for example n8n in a paid customer-facing path; for an internal automation server the Community layout above is enough. The compliance side is in our n8n on-premise for companies post.

Frequently asked questions

How do I set up high availability for a self-hosted n8n instance?

Run n8n in queue mode: PostgreSQL and Redis, one main, one or more workers (n8n worker) and a webhook processor (n8n webhook), all with EXECUTIONS_MODE=queue and the same N8N_ENCRYPTION_KEY. Give every container restart: always and a health check, route /webhook/* to the processor, put workers on a second server over WireGuard, and monitor /healthz from outside. Two mains at once (multi-main) need an Enterprise licence.

Is n8n queue mode free in the Community edition?

Yes. The n8n documentation lists queue mode as included in the Community edition and names only multi-main and the Settings > Workers view as licensed. We ran one main, two workers and a webhook processor on n8n 2.45.5 without a licence key.

How much RAM does n8n queue mode need?

At idle in our test: 358 MiB for the main, about 310 MiB per worker, 254 MiB for a webhook processor, 57 MiB for Postgres and 16 MiB for Redis. Under a burst of 200 webhook executions the workers peaked near 450 MiB each and the main at 410 MiB. Plan 2 GB for main, two workers and the databases, and 0.5 GB per extra worker.

What happens to running executions when an n8n worker crashes?

They end as "crashed" and are not re-run. A worker we killed mid-burst had 11 executions in hand: 3 finished before the kill, 2 were marked crashed at once and 8 within five minutes. n8n 2.0 removed Bull's automatic retry of stalled jobs, so retries must come from an Error Workflow or the sender.

Can n8n workers run on a different server than the main instance?

Yes. A worker only needs Redis and PostgreSQL, so publish both on a private WireGuard address on the main server, give the worker the same N8N_ENCRYPTION_KEY, n8n version and passwords, and start it with n8n worker --concurrency=10. Never publish Redis or Postgres on a public IP; Docker's published ports bypass ufw.

What does n8n multi-main cost?

The n8n pricing page (read 28 October 2026) places multi-main under Enterprise with "Contact Sales" and no public price; the self-hosted Business plan at 667 € a month does not list it. The Community edition gives one main with unlimited workers at no cost.

Start with one main and two workers

Queue mode is the biggest reliability step a self-hosted n8n can take for free, and it fits on a VPS Mini to start. Tell us roughly how many executions a day and whether Code nodes are heavy, and we will say which plan and city fit, or install the setup above for you, quoted per job. Plans are on the n8n VPS hosting page; the fastest way to ask is Telegram.

← All articles

Chat on Telegram