App Hosting

n8n Queue Mode Tested: 2.4x Throughput vs Regular Mode

Dr. Somya Hallan · Nov 15, 2026 · 33 min read
n8n Queue Mode Tested: 2.4x Throughput vs Regular Mode

Tested by M. Aziz, backend engineer at Selfhost.dev · n8n 2.40.7, Postgres 17.10 and Redis 7.4.6 on a Selfhost.dev server in Falkenstein, Germany, November 2026 ·

Our 4 vCPU server was failing n8n requests by the thousand, and three of its cores had nothing to do. In regular mode, n8n pushed every Code node through a single task runner pinned to one core, while main’s memory climbed to 6.9 GB. n8n queue mode hands each execution to worker processes through Redis.

With 2 workers, the same server answered 519 executions a minute at 50 requests a second with zero errors, 2.4 times regular mode on that Code-heavy test. With 4 workers, the errors came back.

That’s the part most guides skip: queue mode isn’t “more workers, more speed”. Below one busy core it only adds RAM, and one setting in regular mode may be all you need. Below are the numbers to decide, the tested Compose file and six failure drills, all on n8n 2.40.7.

KEY FACTS

  • Throughput: on one 4 vCPU / 8 GB server, n8n 2.40.7 queue mode with 2 workers answered 519 Code-heavy executions a minute at 50 requests/s, zero errors; regular mode answered 215.
  • Low load: at 5 requests/s both modes answered about 295 a minute at the same 0.9 s median; queue mode added 227 to 901 MB of idle RAM.
  • Workers: on 4 vCPUs, 2 workers beat 1 (regular mode’s ceiling) and 4 (1,013 errors at 50 requests/s).
  • Concurrency: keep the default of 10; at 5, two workers fell to 426 a minute with 238 errors.
  • Binary data: no S3 needed; in queue mode n8n stores binary data in Postgres by default.
  • Redis: with AOF on, 20 of 20 waiting executions survived a Redis restart; without it, all 20 crashed.

Jump to the tested Compose file

On this page: What it is · vs regular mode · Benchmarks · Common advice vs measured · The cheaper fix · Worker concurrency · Compose file · SQLite to Postgres · Redis · Postgres connections · Failure drills · Monitoring · Field notes · Checklist · FAQ

What is n8n queue mode?

n8n queue mode is an execution mode in which the main instance hands executions to separate worker processes through a Redis queue, with Postgres as the shared database. You turn it on with EXECUTIONS_MODE=queue on every n8n process and start the workers with n8n worker.

What happens to one webhook call:

  1. Main receives it, writes the execution to Postgres and puts its ID on the Redis queue.
  2. A free worker takes the job, reads the workflow from Postgres and runs it, with Code nodes in its own task runner.
  3. The worker writes the result to Postgres; if the webhook waits for the last node, the reply returns to main through Redis.
n8n queue mode architecture: a webhook call reaches the main instance, which writes the execution to Postgres and puts its ID on the Redis queue; one of two workers takes the job, reads the workflow from Postgres, runs it with its task runner and writes the result back.
How n8n queue mode handles one webhook call: main queues it in Redis, a worker runs it, Postgres keeps the state.

The parts:

  • Main instance: editor, API, triggers and webhooks; no production executions.
  • Redis: the queue (n8n 2.40.7 uses the Bull 4 library, not BullMQ).
  • Workers: run executions, up to --concurrency each (default 10).
  • Postgres: shared by every process; SQLite is not supported.
  • Task runners: in n8n 2.x, Code nodes run in a separate runner, so each worker comes as a pair: worker + runner.

We skipped the optional webhook processors (n8n webhook): they help across several machines, but on one server they compete for the same CPUs.

n8n queue mode vs regular mode

Regular mode runs everything in one n8n process with one task runner; queue mode splits the work across worker processes, each with its own runner. On our server that was the whole difference: the single runner used one CPU core of four.

Regular modeQueue mode
What runs executionsThe main n8n processWorker processes (n8n worker)
Code nodes run inOne task runner (one core)One task runner per worker
Max parallel runsNo limit by default (N8N_CONCURRENCY_PRODUCTION_LIMIT=-1)Workers × --concurrency (default 10 each)
When a heavy workflow runsShares CPU with the editor and every webhookRuns on a worker; main and the editor stay responsive
Where a backlog waitsIn main’s memory (354 MB → 6.9 GB in our test)In Redis (3.4 MB at most in our test)
DatabaseSQLite or PostgresPostgres only
Extra servicesNoneRedis, workers, one runner per worker
Failure pointsn8n, its databaseAlso Redis and each worker
Stack RAM at idle (measured)476 MB (main + runner + Postgres)703 MB (1 worker), 919 MB (2), 1,377 MB (4), Redis included

At idle on n8n 2.40.7, queue mode with 2 workers used 919 MB for the whole stack, against 476 MB for regular mode on the same server. For the RAM one n8n instance needs before any of this, see how much RAM a single n8n instance needs.

Do you need n8n queue mode? Benchmarks with 1, 2 and 4 workers

Yes, once your Code-heavy load outgrows one CPU core; no, below that. At 5 requests a second every setup answered about 295 executions a minute. At 20 and 50, regular mode broke and queue mode with 2 workers held.

How we tested

ItemWhat we used
ServerSelfhost.dev Projects server: 4 vCPU (shared, x86), 8 GB RAM (7.7 GB usable), Falkenstein, Germany
Versionsn8n 2.40.7, n8n runners 2.40.7, Postgres 17.10, Redis 7.4.6, all pinned
Test workflowWebhook (respond when last node finishes) → Code node (fixed integer loop, 187 ms warm on this server) → Wait 0.5 s (a slow API call) → Set
Load generatork6 v0.43.1 on a laptop in India, a separate machine; about 320 ms HTTP round trip to the server
LoadConstant arrival rate of 5, 20 and 50 requests/s for 3 minutes each, 120 s client timeout, at most 1,000 requests in flight
Per runFresh deploy, 10 warm-up requests, 20 s idle before and after the load
MeasuredAnswered executions per minute, p50/p95/p99 response time, errors by type, CPU and RAM per container (cgroup counters sampled every second), Redis memory and queue length, Postgres connections, editor responsiveness

Editor responsiveness is the time to load one workflow over the editor’s own REST call (GET /rest/workflows/:id), every 5 s. Each cell is one run. We ran the set twice, on Postgres 16 and then 17 (shown); cells moved by up to 25%, so read smaller gaps as noise.

n8n performance at 5, 20 and 50 requests a second

All five setups ran on the same server with the same workflow, in 3-minute runs at a constant request rate. “Answered/min” counts requests that returned HTTP 200 within 120 s, per minute of load. Errors are HTTP 500/503 from n8n or client timeouts.

Setup5 req/s: answered/min · p9520 req/s: answered/min · errors · p9550 req/s: answered/min · errors · p95Peak RAM, whole stackEditor load p95 under load
Regular, defaults297 · 2.6 s268 · 1,045 · 90 s215 · 3,851 · 100 s7.1 GB21 to 41 s
Regular, N8N_CONCURRENCY_PRODUCTION_LIMIT=10295 · 2.3 s331 · 1 · 114 s112 · 749 · 120 s0.9 GB0.42 s
Queue, 1 worker295 · 2.7 s314 · 51 · 119 s114 · 750 · 120 s1.2 GB0.40 s
Queue, 2 workers295 · 1.0 s513 · 0 · 81 s519 · 0 · 111 s1.8 GB0.55 s
Queue, 4 workers295 · 1.0 s557 · 0 · 74 s505 · 1,013 · 93 s2.8 GB1.3 to 8.7 s

On a 4 vCPU / 8 GB server, n8n 2.40.7 in regular mode answered 268 executions a minute at 20 requests/s with 1,045 errors; queue mode with 2 workers answered 513 a minute with none.

At 50 requests/s, queue mode with 2 workers answered 519 executions a minute with zero errors, 2.4 times regular mode’s 215.

n8n queue mode benchmark at 50 requests a second on one 4 vCPU server: executions answered per minute were 215 in regular mode (3,851 errors), 112 in regular mode with a concurrency limit of 10 (749 errors), 114 with 1 queue mode worker (750 errors), 519 with 2 workers (0 errors) and 505 with 4 workers (1,013 errors).
n8n queue mode at 50 requests a second: 2 workers answered the most executions with zero errors; a third and fourth worker added errors, not throughput.

What happened in each setup:

  • Regular, defaults: the one task runner sat at 100% of one core while three more were free. Main held every waiting request in memory, growing from 354 MB to 6.9 GB. At 20 requests/s the 1,045 errors included 726 HTTP 500s and 285 HTTP 503s, and the editor took 21 s to load a workflow.
  • Queue, 1 worker: the same single-core ceiling, but the backlog waited in Redis. 51 requests passed the 120 s client timeout at 20 requests/s.
  • Queue, 2 workers: two runners, two cores, zero errors. Main stayed at 440 to 590 MB.
  • Queue, 4 workers: fastest at 20 requests/s (557 a minute), but at 50 no faster than 2 and with 1,013 errors: 714 503 {"code":503,"message":"Database is not ready!"}, 175 HTTP 500s, 124 timeouts. A pool size of 10 removed them in this run but not on Postgres 16 (930); our guess is CPU contention (not proven).

Long p95 values show the backlog, not one execution’s speed, so compare answered/min.

Our numbers sit far below n8n’s documented 220 executions a second on one instance, which used a two-node Webhook and Edit Fields workflow. Ours spends 187 ms of CPU in a Code node and waits 0.5 s on every run. The shape matches, though: in n8n’s scalability benchmark, a 16 vCPU machine reached 16.2 requests a second in single mode and 162 in queue mode.

What these numbers don’t cover:

  • One Code-heavy workflow on one shared 4 vCPU server. Workflows that mostly wait on APIs, query databases or move large files may scale differently.
  • One run per cell, with up to 25% variation between repeat runs.
  • Response times include about 320 ms of network between our load generator and the server.
  • n8n 2.40.7. Later versions changed how workers claim jobs and shut down, so re-check the failure drills on yours.

Treat 2 workers per 4 vCPU as a starting point, then measure your own workflows.

Common advice vs what we measured

Five common pieces of queue mode advice didn’t hold on our server.

Common adviceWhat we measured (n8n 2.40.7, one 4 vCPU / 8 GB server)
“Queue mode on one server adds no computing power”For Code-heavy work it does: regular mode ran every Code node in one runner on one core. 2 workers answered 519 executions a minute at 50 requests/s, 2.4 times regular mode’s 215
“Set worker concurrency to 5”With 2 workers, 5/5 fell to 426 a minute with 238 errors at 50 requests/s; the default of 10 held 519 with none
“Start with one worker”One worker kept the single-runner ceiling: 51 requests timed out at 20 requests/s, against 0 with 2 workers
“More workers, more throughput”4 workers answered no more than 2 at 50 requests/s (505 against 519 a minute) and added 1,013 errors
“Queue mode needs S3 for binary data”n8n’s docs: “In queue mode, n8n uses database by default.” Our worker read a 300 kB upload from Postgres. S3 needs a Business or Enterprise licence

If your server has 4 vCPUs and Code-heavy workflows, start with 2 workers at the default concurrency of 10, not 1 worker at 5.

The cheaper fix: a concurrency limit in regular mode

If a few heavy workflows spike at once, try N8N_CONCURRENCY_PRODUCTION_LIMIT in regular mode before queue mode. A limit of 10 kept main at 674 MB instead of 6.9 GB, held the editor at 0.42 s, and cut errors at 20 requests/s from 1,045 to 1.

The cost is waiting: extra executions queue inside main, first in, first out, and the median caller waited 62 s. It adds no capacity either; at 50 requests/s it answered 112 a minute. Per n8n’s concurrency docs, it counts only webhook and trigger runs, not manual or sub-workflow ones.

n8n worker concurrency: is the default of 10 right?

Yes, on our server. We varied --concurrency together with the runner’s own limit (N8N_RUNNERS_MAX_CONCURRENCY, 5 in the n8n one-click template we started from). 2 workers, each run once:

Worker concurrency / runner limit 20 req/s: answered/min · p95 50 req/s: answered/min · errors
5 / 5 502 · 82 s 426 · 238
10 / 5 (default concurrency, template runner limit) 513 · 81 s 519 · 0
10 / 10 491 · 84 s 517 · 0
20 / 20 499 · 83 s 514 · 0

With 2 workers on 4 vCPUs, 10/5, 10/10 and 20/20 answered 491 to 519 executions a minute, within noise of each other. Only 5/5 fell behind at 50 requests/s (426 a minute, 238 errors). An 18% gain for 10/10 in our Postgres 16 run did not repeat.

CPU was the ceiling: the stack used 3.2 to 3.5 of 4 cores in every 2-worker run at 50 requests/s, so more parallel tasks only waited for the same cores.

One setting overrides the flag. In queue mode, each worker takes its limit from N8N_CONCURRENCY_PRODUCTION_LIMIT when it’s set to anything but -1; it’s per worker, not a total. So a value left over from regular mode quietly changes how many executions each worker takes, whatever --concurrency says. n8n Cloud’s concurrency limits per plan are a separate cap.

When to turn on queue mode: our engineer’s call

Mohammad Aziz, backend engineer at Selfhost.dev, who ran these tests: “If your server runs a few hundred light executions an hour, I’d leave queue mode off. It adds Redis, more containers and 0.2 to 0.9 GB of RAM, and nothing gets faster at that load: at 5 requests/s every setup I tested answered with the same 0.9 s median. I’d turn it on when I see one of two things: the runner container pinned at 100% of one core while the other cores sit idle, or the editor stalling whenever workflows run. On a 4 vCPU server I’d start with 2 workers and leave --concurrency at its default of 10: going lower hurt, going higher didn’t help. I’d stop adding workers once the CPU is full. At 50 requests/s, 2 workers already used 3.5 of 4 cores, and 4 workers added errors, not throughput.”

Every number above came from one 4 vCPU / 8 GB server, with main, 2 workers, Redis and Postgres all on it.
See what that exact server costs per hour, no signup

Set up n8n queue mode with Docker Compose

This is the stack behind every queue mode number above: main, workers, a task runner per n8n process, Redis and Postgres, every version pinned. With 2 workers it runs 8 containers, and it went healthy as one Docker Compose service on a Selfhost.dev Projects server.

Before you copy it: the example serves n8n over plain HTTP on port 5678 with secure cookies off, which is fine for a first test only. For production, put n8n behind HTTPS with a real domain and delete the N8N_SECURE_COOKIE line.

Set five values first, in a .env file or your platform’s variables:

N8N_ENCRYPTION_KEY=$(openssl rand -hex 32)   # same for every n8n process, never change it
POSTGRES_PASSWORD=$(openssl rand -hex 24)
REDIS_PASSWORD=$(openssl rand -hex 24)
RUNNERS_AUTH_TOKEN=$(openssl rand -hex 24)
N8N_PUBLIC_URL=http://<your-server-ip>:5678/
services:
  n8n-postgres:
    image: postgres:17.10-alpine
    restart: unless-stopped
    environment:
      POSTGRES_USER: n8n
      POSTGRES_PASSWORD: "${POSTGRES_PASSWORD}"
      POSTGRES_DB: n8n
    volumes:
      - postgres-data:/var/lib/postgresql/data
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U n8n -d n8n"]
      interval: 5s
      timeout: 5s
      retries: 10

  n8n-redis:
    image: redis:7.4.6-alpine
    restart: unless-stopped
    command: redis-server --requirepass ${REDIS_PASSWORD} --appendonly yes --appendfsync everysec --maxmemory-policy noeviction
    environment:
      REDISCLI_AUTH: "${REDIS_PASSWORD}"
    volumes:
      - redis-data:/data
    healthcheck:
      test: ["CMD-SHELL", "redis-cli ping | grep PONG"]
      interval: 5s
      timeout: 5s
      retries: 10

  n8n-main:
    image: n8nio/n8n:2.40.7
    restart: unless-stopped
    environment: &n8n-env
      EXECUTIONS_MODE: queue
      QUEUE_BULL_REDIS_HOST: n8n-redis
      QUEUE_BULL_REDIS_PORT: "6379"
      QUEUE_BULL_REDIS_PASSWORD: "${REDIS_PASSWORD}"
      QUEUE_HEALTH_CHECK_ACTIVE: "true"
      OFFLOAD_MANUAL_EXECUTIONS_TO_WORKERS: "true"
      DB_TYPE: postgresdb
      DB_POSTGRESDB_HOST: n8n-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_RUNNERS_MODE: external
      N8N_RUNNERS_BROKER_LISTEN_ADDRESS: 0.0.0.0
      N8N_RUNNERS_BROKER_PORT: "5679"
      N8N_RUNNERS_AUTH_TOKEN: "${RUNNERS_AUTH_TOKEN}"
      N8N_EDITOR_BASE_URL: "${N8N_PUBLIC_URL}"
      N8N_WEBHOOK_URL: "${N8N_PUBLIC_URL}"
      N8N_SECURE_COOKIE: "false"   # plain HTTP on the bare port; remove behind HTTPS
      EXECUTIONS_DATA_PRUNE: "true"
      EXECUTIONS_DATA_MAX_AGE: "168"
      GENERIC_TIMEZONE: UTC
      TZ: UTC
      N8N_DIAGNOSTICS_ENABLED: "false"
    ports:
      - "5678:5678"
    volumes:
      - n8n-main-data:/home/node/.n8n
    depends_on:
      n8n-postgres: { condition: service_healthy }
      n8n-redis: { condition: service_healthy }
    healthcheck:
      test: ["CMD-SHELL", "wget -qO- http://127.0.0.1:5678/healthz/readiness"]
      interval: 10s
      timeout: 10s
      retries: 30

  runner-main:
    image: n8nio/runners:2.40.7
    restart: unless-stopped
    environment:
      N8N_RUNNERS_AUTH_TOKEN: "${RUNNERS_AUTH_TOKEN}"
      N8N_RUNNERS_MAX_CONCURRENCY: "5"
      N8N_RUNNERS_AUTO_SHUTDOWN_TIMEOUT: "15"
      N8N_RUNNERS_TASK_BROKER_URI: http://n8n-main:5679
    depends_on: [n8n-main]
    healthcheck:
      test: ["CMD-SHELL", "wget -qO- http://127.0.0.1:5680/healthz"]
      interval: 10s
      timeout: 10s
      retries: 10

  n8n-worker-1:
    image: n8nio/n8n:2.40.7
    restart: unless-stopped
    command: worker --concurrency=10
    environment: *n8n-env
    depends_on:
      n8n-main: { condition: service_healthy }
    healthcheck:
      test: ["CMD-SHELL", "wget -qO- http://127.0.0.1:5678/healthz"]
      interval: 10s
      timeout: 10s
      retries: 10
    stop_grace_period: 60s

  runner-worker-1:
    image: n8nio/runners:2.40.7
    restart: unless-stopped
    environment:
      N8N_RUNNERS_AUTH_TOKEN: "${RUNNERS_AUTH_TOKEN}"
      N8N_RUNNERS_MAX_CONCURRENCY: "5"
      N8N_RUNNERS_AUTO_SHUTDOWN_TIMEOUT: "15"
      N8N_RUNNERS_TASK_BROKER_URI: http://n8n-worker-1:5679
    depends_on: [n8n-worker-1]
    healthcheck:
      test: ["CMD-SHELL", "wget -qO- http://127.0.0.1:5680/healthz"]
      interval: 10s
      timeout: 10s
      retries: 10

  # n8n-worker-2 and runner-worker-2: copy the two blocks above, replace -1 with -2

volumes:
  postgres-data:
  redis-data:
  n8n-main-data:

To keep it short, this version shares settings through a YAML anchor and writes out one worker. Exactly as shown, it ran locally with all 6 containers healthy; the full 2-worker file is the one that ran on Selfhost.dev. Any server with Docker Compose can run it, though n8n hosts compared on what breaks n8n differ on everything around it.

Why it looks like this:

  • Workers are separate services, not deploy.replicas: Selfhost.dev’s Compose route doesn’t accept deploy:, and each worker needs its own runner.
  • Postgres and Redis are named n8n-postgres and n8n-redis. On a Coolify-based host (Selfhost.dev included), postgres and redis already point at the platform’s own services (field note 1).
  • Every runner has a healthcheck, or the platform marks the service failed after 10 minutes (field note 2).

What changes if you run it on Selfhost.dev’s Compose route:

Selfhost.devYou
The serverProvisioned and set up for youPick the size
Deploying the stackFrom your repo’s Compose file, redeployed on every pushKeep the file in a repo
Domain and HTTPSCustom domain with TLS issued and renewedPoint your domain
Logs and metricsLive, per serviceRead them
n8n, Redis and Postgres settingsSet in your Compose file and variables
Your workflowsBuild and run them

This stack went healthy on that route: 8 containers on one 4 vCPU / 8 GB server, with the domain and TLS handled for you.
Run this queue mode stack on a server we set up
File already in a repo? Deploy it with the $5 welcome credit, no card

n8n queue mode environment variables and task runners

Every n8n process needs the same queue, database and key settings; each runner needs only its token and its own process’s address.

VariableValue we usedNeeded onWhat breaks without it
EXECUTIONS_MODEqueuemain, workersMain runs everything itself; workers have nothing to do
QUEUE_BULL_REDIS_HOST / _PORT / _PASSWORDn8n-redis / 6379 / secretmain, workersDefault host is localhost: nothing connects. A wrong password logs WRONGPASS invalid username-password pair
DB_TYPEpostgresdbmain, workersDefault is sqlite. A wrong value such as postgres is ignored and n8n silently starts on SQLite
DB_POSTGRESDB_HOST / _DATABASE / _USER / _PASSWORDn8n-postgres / n8n / n8n / secretmain, workersNo shared database
N8N_ENCRYPTION_KEY64 hex charactersmain, workersEach process makes its own key; a worker with a different key refuses to start (failure drills)
QUEUE_HEALTH_CHECK_ACTIVEtrueworkersNo /healthz on workers, so no healthcheck (default false)
OFFLOAD_MANUAL_EXECUTIONS_TO_WORKERStruemainRuns from the editor’s Execute button run on main (default false)
N8N_RUNNERS_MODE + broker settingsexternal, 0.0.0.0:5679main, workersThe runner container cannot connect
N8N_RUNNERS_AUTH_TOKENsecretevery n8n process and runnerRunners are refused
N8N_RUNNERS_TASK_BROKER_URIhttp://<its own n8n service>:5679each runnerA runner pointed at main serves only main; workers’ Code nodes wait
N8N_EDITOR_BASE_URL, N8N_WEBHOOK_URLthe public URLmainWebhook URLs shown in the editor use localhost

Start order: main before workers

Main starts first, after Postgres and Redis report healthy; workers start after main is healthy. Main runs the database migrations on first start.

We also started a worker first, on an empty database. In n8n 2.40.7 it logged Acquiring database migration lock..., ran the migrations itself and reported n8n worker is now ready. Keep the depends_on order anyway, so one process migrates.

Check that a worker picked up the execution

A worker that is ready logs:

n8n worker is now ready
 * Version: 2.40.7
 * Concurrency: 10
 * Pool: (default)
 * Queue: jobs
Registered runner "launcher-javascript" (f018d708cb798768)
Registered runner "launcher-python" (34a3bca497971c72)

The two Registered runner lines show the worker’s own runner connected. Trigger a workflow; main logs Execution 5642 (job 5641) finished and a worker logs Worker started execution 1 (job 1) and Worker finished execution 1 (job 1). Each worker also answers GET http://<worker>:5678/healthz inside the Docker network.

Coming from SQLite? Move n8n to Postgres first

Queue mode needs Postgres. In our local test, n8n 2.40.7’s export:entities and import:entities moved a whole SQLite instance: owner login, active workflows, credentials and execution history.

# 1. Stop n8n, then export from the SQLite volume (same key as the running instance)
docker run --rm -v n8n_data:/home/node/.n8n -v "$PWD/exports":/exports \
  -e N8N_ENCRYPTION_KEY="$KEY" --entrypoint n8n n8nio/n8n:2.40.7 \
  export:entities --outputDir=/exports --includeExecutionHistoryDataTables=true

# 2. Start n8n once on Postgres (DB_TYPE=postgresdb, same N8N_ENCRYPTION_KEY) so it creates the schema, stop it, then:
docker run --rm --network <net> -v "$PWD/exports":/exports -v n8n_pg_data:/home/node/.n8n \
  -e DB_TYPE=postgresdb -e DB_POSTGRESDB_HOST=n8n-postgres -e DB_POSTGRESDB_DATABASE=n8n \
  -e DB_POSTGRESDB_USER=n8n -e DB_POSTGRESDB_PASSWORD="$PGPASS" -e N8N_ENCRYPTION_KEY="$KEY" \
  --entrypoint n8n n8nio/n8n:2.40.7 import:entities --inputDir=/exports --truncateTables

# 3. Move the execution id sequence past the imported rows
psql -U n8n -d n8n -c "SELECT setval(pg_get_serial_sequence('execution_entity', 'id'), (SELECT max(id) FROM execution_entity));"
  • Execution history is left out by default. Add --includeExecutionHistoryDataTables=true (our small instance: 176 KB without, 184 KB with).
  • DB_TYPE=postgres does not fail. n8n logs Invalid value for DB_TYPE - Invalid enum value. Expected 'sqlite' | 'postgresdb', received 'postgres'. Falling back to default value. and starts a fresh SQLite database, so your import seems to vanish.
  • The first new execution failed with QueryFailedError: duplicate key value violates unique constraint "pk_e3e63bbf986767844bbe1166d4e", because the id sequence was still at 1. Step 3 fixed it, and the imported credential worked.
  • A different encryption key is refused: Mismatching encryption keys. The encryption key in the settings file /home/node/.n8n/config does not match the N8N_ENCRYPTION_KEY env var. Copy the old key from /home/node/.n8n/config first.

If you only need Postgres and not queue mode, you can stop after step 3: regular mode runs on Postgres too, and our n8n template starts on its own Postgres from the first boot.

Redis for n8n queue mode: the settings that matter

Redis held at most 3.4 MB in any run, so size is not the issue: persistence and eviction are. Without persistence, a Redis restart lost every waiting job in our test; with AOF, none.

Redis eviction policy: why it must be noeviction

n8n 2.40.7 queues through the Bull 4 library, whose jobs live in ordinary Redis keys (bull:jobs:wait, bull:jobs:active, one hash per job). If Redis has a memory limit and an evicting policy, it can delete those keys. Set it explicitly:

redis-server --maxmemory-policy noeviction

With allkeys-lru set on purpose, neither n8n nor Redis logged a warning, so nothing tells you the setting is wrong until a key disappears. (We didn’t fill Redis to its limit, so eviction itself wasn’t reproduced.)

Redis persistence: what survives a restart

We queued 20 executions with no worker running, so they waited in Redis, then killed Redis with SIGKILL and started it again:

Redis settingWaiting before the killWaiting after restartOutcome once a worker came back
--appendonly yes --appendfsync everysec2020All 20 ran, success
--appendonly no --save ''200All 20 marked crashed

With AOF persistence on, 20 of 20 waiting n8n executions survived a Redis kill and ran afterwards. Without it, they show as crashed (the rows live in Postgres) and never run again on their own.

While Redis was down, main logged Lost Redis connection. Trying to reconnect in 1s... (0s/10s), then Unable to connect to Redis after trying to connect for 10s, and exited. restart: unless-stopped brought it back 4 s after Redis returned. Webhooks sent during the outage got no answer.

How much memory Redis used

LoadRedis used memory
Idle (1 to 4 workers)2.1 to 2.4 MB
20 req/s, up to about 1,000 jobs waiting3.0 to 3.3 MB
50 req/s, up to about 1,400 jobs waiting3.1 to 3.4 MB

A larger backlog would use more (our load generator capped open requests at 1,000), but execution data lives in Postgres, not Redis.

Postgres connections for n8n workers: the pool size math

Plan connections with this formula:

connections = (main pool + 1) + workers × worker pool + headroom (psql, backups, monitoring)

In plain terms: with n8n’s default pool (DB_POSTGRESDB_POOL_SIZE=2), main holds 3 Postgres connections and each worker 2. That held in every run; only 4 workers at 50 requests/s went over, once, by one:

SetupConnections from n8n (peak, pg_stat_activity)
Regular mode3
Queue, 1 worker5
Queue, 2 workers7
Queue, 4 workers11 (12 once)
Queue, 4 workers, DB_POSTGRESDB_POOL_SIZE=1044

With the defaults that is 13 for 4 workers, far below Postgres’s default max_connections of 100. Raising the pool to 10 on 4 workers took it to 44.

Aziz on PgBouncer: “I wouldn’t put PgBouncer in front of n8n on one server with a handful of workers. Even 4 workers with raised pools stayed at 44 connections against Postgres’s 100. I’d add it when connections come from several servers. If you do, transaction pooling works: main and 2 workers behind PgBouncer 1.24.1 ran 200 of 200 webhooks with no errors, on 6 server connections. That was a short functional test, not part of the benchmark.”

Execution data growth and pruning

Our tiny workflow stored 3.1 to 3.8 KB per execution: 15 to 20 MB per setup’s three runs. With EXECUTIONS_DATA_PRUNE=true (the default), n8n deletes executions older than EXECUTIONS_DATA_MAX_AGE (336 hours) or beyond EXECUTIONS_DATA_PRUNE_MAX_COUNT (10,000).

At about 550 executions a minute, 10,000 is only 18 minutes of history. Decide which limit you hit first; our file sets 168 hours and keeps the 10,000 cap. Real payloads are bigger than ours, so measure yours.

What breaks in n8n queue mode: 6 failure drills

In n8n queue mode, a worker that dies or stops slowly loses the executions it was running: n8n marks them failed and does not retry them. Everything else we broke recovered on its own. We ran these drills locally in Docker (29.8.0) with the same Compose file, n8n 2.40.7, 1 to 2 workers at concurrency 10.

What we brokeWhat happened
Killed a worker mid-executionIts 4 running executions failed after about 70 s; the other worker carried on
Started a worker with another encryption keyIt refused to start
Restarted Redis with 20 jobs waitingAOF kept all 20; without it, all 20 crashed
Restarted Postgres under load8 of 361 requests failed; everything recovered on its own
Used filesystem binary data without a shared volumeThe worker couldn’t read the file (HTTP 500)
Stopped a worker normallyExecutions longer than 30 s were cut off

1. Kill a worker mid-execution

Four executions of a workflow with a 50 s Wait were running on worker-1; we sent it SIGKILL (docker kill).

  • They showed running, then about 70 s later all 4 turned to error, not retried on the other worker. Main logged:
Problem with execution 2: This execution failed to be processed too many times and will no longer retry. To allow this execution to complete, please break down your workflow or scale up your workers or adjust your worker settings.. Aborting
Error: job stalled more than maxStalledCount (execution 2)
  • The other worker kept serving new executions.
  • Docker did not restart the killed worker: docker kill counts as a manual stop for restart: unless-stopped, so it stayed exited (137).

2. Worker with a different encryption key

In n8n 2.40.7 the worker does not start at all, so it cannot run anything with the wrong key:

Error: Deployment key 'signing.hmac' cannot be read with this instance encryption key
Worker exiting due to an error.

The other worker ran all 8 test executions that used a credential, all success. Check this line if a new worker never shows up as ready.

3. Redis restart with jobs waiting

With AOF on, 20 of 20 waiting executions survived a SIGKILL of Redis; with persistence off, all 20 ended as crashed. Main exits after 10 s without Redis and needs a restart policy to come back. The logs and table are in the Redis persistence section.

4. Postgres restart during load

We restarted Postgres at 4 requests a second. It was back in 1 s and main was ready again in 3 s. 8 of 361 requests failed: 7 with 503 {"code":503,"message":"Database is not ready!"} and 1 HTTP 500. Main and the worker logged Postgres pool client error: terminating connection due to administrator command, then Database connection recovered, with no manual restart.

5. Binary data on the filesystem without a shared volume

With N8N_DEFAULT_BINARY_DATA_MODE=filesystem and no shared volume, a 300 kB upload read by a Code node (this.helpers.getBinaryDataBuffer) landed on main’s disk. The worker couldn’t read it, and the caller got {"message":"Error in workflow"} (HTTP 500). The node error is only the missing path:

{"level":"info","shouldReport":false,"tags":{},"extra":{"filePath":"workflows/<workflow id>/executions/temp/binary_data/<uuid>"}}

With the variable unset, n8n 2.40.7 stored the upload in Postgres (id: database:…) and the worker read all 300,000 bytes, locally and on Selfhost.dev. That’s by design: unset, n8n picks database in queue mode and filesystem in regular mode (binary-data.config.ts in n8n 2.40.7), and n8n’s docs now say so. Filesystem mode needs main and every worker on one shared volume. Database mode takes files up to 512 MiB (N8N_BINARY_DATA_DATABASE_MAX_FILE_SIZE).

6. Graceful shutdown

We stopped a worker normally (docker stop, SIGTERM, with the Compose stop_grace_period: 60s) while it ran two 20 s and two 50 s executions.

Waiting for 2 active executions to finish... (execution IDs: 7, 8)
Shutdown timed out after 30 seconds
process exited after 30s

The 20 s executions finished; the 50 s ones were cut off and marked error. n8n’s N8N_GRACEFUL_SHUTDOWN_TIMEOUT (default 30 s) decided, not Docker’s 60 s, so raise both above your longest execution.

How to monitor n8n queue mode

Turn on n8n’s queue metrics on main and watch three numbers: jobs waiting, the runner containers’ CPU and main’s memory. Those moved first in every run that went wrong.

On main:

N8N_METRICS=true
N8N_METRICS_INCLUDE_QUEUE_METRICS=true   # updated every N8N_METRICS_QUEUE_METRICS_INTERVAL (default 20 s)

GET /metrics on main then includes:

n8n_scaling_mode_queue_jobs_waiting 520
n8n_scaling_mode_queue_jobs_active 20
n8n_scaling_mode_queue_jobs_completed 5436
n8n_scaling_mode_queue_jobs_failed 0

That snapshot is from the 2-worker run at 50 requests/s: active = 20 is exactly 2 workers × concurrency 10, so the workers were full. /metrics has no login and sits on main’s public port, so expose it only to your monitoring server.

Also watch worker /healthz and /healthz/readiness (with QUEUE_HEALTH_CHECK_ACTIVE=true), redis-cli LLEN bull:jobs:wait for the queue without n8n, and executions marked error or crashed.

The 3 numbers to alert on

Alert onThreshold we would setWhat we saw
n8n_scaling_mode_queue_jobs_waitingAbove 0 and rising for 5 minutesHealthy runs drained to 0; overloaded runs climbed to about 1,000
Runner container CPUAbove 90% of one core for 5 minutes (regular mode), or the whole server above 85% (queue mode)Regular mode’s runner at 100% of one core while 3 cores idled; 2 workers ran at 3.5 of 4 cores with no errors; 4 workers at 3.6 cores began failing
503 Database is not ready! from mainAny, sustained over 1 minuteOnly when 4 workers overloaded 4 vCPU, and for 3 s during a Postgres restart

Tune these once you know your normal queue depth. In regular mode, also alert on main’s memory: it went from 0.35 GB to 6.9 GB in about 3 minutes.

Field notes: what our engineer got wrong the first time

From Mohammad Aziz’s build log, in his words:

  1. “I named the services postgres and redis. Main came up fine, both workers did not: [Redis client] WRONGPASS invalid username-password pair or user is disabled. and password authentication failed for user "n8n". Our own Postgres logged no failed login, which was the clue: on a Coolify-based host every container also joins the shared coolify network, where postgres and redis are the platform’s own. Renaming them fixed it. It cost about 40 minutes, partly because I first blamed my YAML anchors.”
  2. “I gave the runners no healthcheck. Everything else was healthy, but the runners reported running:unknown and the platform failed the whole service after 10 minutes. One wget healthcheck per runner fixed it.”
  3. “The first request after a quiet minute was slow. The task runner shuts itself down after 15 s idle (N8N_RUNNERS_AUTO_SHUTDOWN_TIMEOUT=15). The first Code node after idle took 2,709 ms instead of 187 ms, and the first webhook answered in 7.0 s instead of about 1.1 to 1.6 s.”

n8n queue mode checklist

Ten checks before the first production webhook, each from a test above:

  • Postgres with DB_TYPE=postgresdb exactly (postgres silently falls back to SQLite).
  • One N8N_ENCRYPTION_KEY on main and every worker.
  • Every image pinned; main starts after Postgres and Redis, workers after main.
  • One task runner per n8n process, pointed at its own process, with a healthcheck.
  • Redis with noeviction and --appendonly yes.
  • Postgres connections: (main pool + 1) + workers × pool, under max_connections.
  • --concurrency at 10, and no more workers than your cores can feed (2 per 4 vCPU in our test).
  • Binary data mode unset or database, not filesystem without a shared volume.
  • Pruning set for your volume; shutdown timeouts above your longest execution.
  • Alerts on jobs waiting, runner CPU and Database is not ready!; /metrics closed to the internet.

The tested file already covers all 10.
Start with a stack that passes every check, no card needed

n8n queue mode FAQ

How many n8n workers should I run on one server?

As many as your CPU cores can feed, leaving room for main and Postgres. On a 4 vCPU server running Code-heavy workflows, 2 workers answered 519 executions a minute at 50 requests a second with no errors. 4 workers answered no more at the same load and added 1,013 errors.

Can n8n queue mode run on one server?

Yes. Every number in this post comes from main, 2 workers, Redis and Postgres on one 4 vCPU / 8 GB server. On one server, queue mode spreads executions across CPU cores, but it doesn’t give you high availability: if the server goes down, everything does.

What is the default worker concurrency in n8n?

10 executions per worker (n8n worker --concurrency=10). In n8n 2.x the task runner has its own limit, 5 in n8n’s one-click template. In our test, raising both to 10 or 20 made no difference beyond noise, while lowering both to 5 cost throughput at 50 requests a second.

Does n8n queue mode need S3 for binary data?

No. In queue mode, n8n stores binary data in Postgres by default, and in our test a worker read a 300 kB upload from it. Database mode takes files up to 512 MiB. S3 needs a Business or Enterprise licence. Filesystem mode only works if main and every worker share one volume.

Do manual executions run on workers?

Not by default. OFFLOAD_MANUAL_EXECUTIONS_TO_WORKERS defaults to false, so runs from the editor’s Execute button stay on main, while webhook and trigger runs go to workers. Set it to true on main, as our Compose file does, if you test heavy workflows from the editor.

Does N8N_CONCURRENCY_PRODUCTION_LIMIT work in queue mode?

Yes, but per worker. Per n8n’s docs, when it’s set to anything other than -1, each worker takes its limit from it instead of --concurrency. It isn’t a total across workers. In regular mode it caps production runs inside main; a limit of 10 kept our main at 674 MB.

Do n8n workers need the same encryption key?

Yes. In n8n 2.40.7 a worker with a different N8N_ENCRYPTION_KEY stops at boot with Deployment key 'signing.hmac' cannot be read with this instance encryption key. Copy the key from main’s /home/node/.n8n/config into every worker before you start them, and never change it afterwards.

Does n8n queue mode need an Enterprise licence?

No. Queue mode itself isn’t licensed; we ran it on the free Community image. Multi-main setup and the Settings > Workers view need a licence, and external binary storage (S3) is for Business and Enterprise plans. For the rest, see what the free Community edition includes.

The short version

n8n queue mode pays off once one task runner can’t keep up, and not before:

  • CPU and memory comfortable, editor responsive: stay in regular mode.
  • A few heavy workflows spiking at once: set N8N_CONCURRENCY_PRODUCTION_LIMIT first.
  • One runner pinned at a full core: switch to queue mode, starting with 2 workers per 4 vCPU at concurrency 10.
  • The whole server near full CPU: add cores, not workers.

Still below one busy core? Run n8n on a server we keep running, no Redis to look after. Past it? Put main, workers and Redis on one server we set up.

Keep reading:

Ship the app you just read about.

Deploy straight from GitHub onto a dedicated server we run for you, with auto-deploy on push and preview environments. Pay by the hour from about $0.02 in Europe, with no surprise invoices. No DevOps. Limited-time offer: get $5 in free credit at signup, no card needed.

cost check