Artur
Artur
Founder

n8n Queue Mode: Scale With Workers & Redis (2026)

June 10, 2026

n8n-queue-moden8n-scalingn8n-workersn8n-redisself-hosting

TL;DR: Queue mode splits n8n into a main process that receives triggers and webhooks, one or more worker processes that run the executions, and Redis as the message broker between them. You enable it with EXECUTIONS_MODE=queue on every process, point them all at the same Redis and the same Postgres database, share one N8N_ENCRYPTION_KEY, and start workers with n8n worker --concurrency=10. It is worth it once a single n8n process starts queuing executions behind each other or maxing CPU - not before. Below: the architecture, the verified env vars, a copy-paste docker-compose, and how to tune concurrency without exhausting your database connection pool.

When you actually need queue mode

Switch to queue mode when a single n8n process can no longer keep up with concurrent executions, not because a guide told you to. The default deployment runs in regular mode, where one Node.js process handles the UI, the triggers, the webhooks, and the actual workflow execution. That single process is fine for a surprising amount of load. Most self-hosted instances never outgrow it.

The signals that you have outgrown it are concrete. Executions start stacking up and finishing minutes after they were triggered. CPU on the box sits pinned near 100% during busy periods. A long-running workflow (a big AI batch, a slow API loop) blocks everything else from starting because they are all sharing one event loop. Webhooks time out under burst traffic. If you are seeing those symptoms, queue mode is the fix. If you are not, it is premature complexity: you have just turned one container into three plus a Redis instance, and you now own the failure modes of a distributed system.

One more decision gate: queue mode requires Postgres. Running it on SQLite is explicitly unsupported because multiple processes hammering one SQLite file does not end well. If you are still on the default SQLite database, fix that first - see n8n Docker + Postgres setup and, if you have existing data, migrating n8n from SQLite to PostgreSQL. And if you are still deciding whether self-hosting is even the right call at your volume, the math in n8n Cloud vs self-hosted is the better first read.

The architecture: main, workers, Redis

Queue mode is one main process generating work, Redis holding the queue, and workers draining it. Here is the actual flow, straight from how n8n behaves:

  1. The main process handles timers and webhook calls and generates a workflow execution - but does not run it.
  2. It pushes the execution ID into Redis, which keeps the queue of pending jobs.
  3. A free worker pulls the job from Redis.
  4. The worker reads the full workflow and credentials from Postgres using that execution ID.
  5. The worker runs the workflow, writes results back to Postgres, and posts to Redis that the job finished.
  6. Redis notifies the main process.

Two consequences fall out of this design. First, every worker needs access to both Redis and the same Postgres database the main process uses - they are not optional dependencies, they are the spine. Second, because credentials live encrypted in Postgres and workers have to decrypt them, every process must share the same encryption key. Miss that and your workers will pull jobs and then fail to decrypt credentials, which is a confusing failure to debug after the fact.

There is also an optional third process type: the dedicated webhook processor. More on that below, but for most people the right starting topology is one main and two workers.

Binary data caveat

Queue mode does not support filesystem binary data storage. If your workflows persist binary data (file downloads, generated PDFs, images), the worker that produced the file and the main process that serves it are different processes on potentially different machines, so a local filesystem path means nothing across them. Use S3-compatible external storage instead, configured identically on every process. Skip this and you get files that exist on one node and 404 everywhere else.

The env vars that actually matter

Every process - main, worker, and webhook - needs the same core configuration. These names are exact; do not guess at them.

Set the mode and Redis connection on all processes:

export EXECUTIONS_MODE=queue
export QUEUE_BULL_REDIS_HOST=redis
export QUEUE_BULL_REDIS_PORT=6379
export QUEUE_BULL_REDIS_PASSWORD=your-redis-password
export QUEUE_BULL_REDIS_DB=0

QUEUE_BULL_REDIS_HOST defaults to localhost and QUEUE_BULL_REDIS_PORT to 6379. Username (QUEUE_BULL_REDIS_USERNAME) and password are unset by default; set the password in any real deployment. If you run Redis Cluster, set QUEUE_BULL_REDIS_CLUSTER_NODES as a comma-separated list of host:port entries - n8n then ignores host and port and creates a cluster client. For managed Redis behind TLS, set QUEUE_BULL_REDIS_TLS=true.

Share the encryption key. n8n generates one automatically on first start, but in a multi-process setup you must pin it explicitly so every process agrees:

export N8N_ENCRYPTION_KEY=<your-shared-encryption-key>

A few more that are worth knowing rather than memorizing:

  • N8N_GRACEFUL_SHUTDOWN_TIMEOUT (default 30 seconds) controls how long a worker waits for in-flight executions to finish before it exits. The old QUEUE_WORKER_TIMEOUT is deprecated; use this one.

  • QUEUE_HEALTH_CHECK_ACTIVE (default false) turns on the worker's /healthz and /healthz/readiness endpoints. Set it to true so your orchestrator can probe workers.

  • QUEUE_BULL_REDIS_TIMEOUT_THRESHOLD (default 10000 ms) is how long n8n waits on an unreachable Redis before exiting.

  • OFFLOAD_MANUAL_EXECUTIONS_TO_WORKERS (default false) sends manual (editor "Test workflow") runs to workers too, instead of running them on main.

A docker-compose you can copy

This is a minimal but complete queue-mode stack: Postgres, Redis, one main process, and two workers. Tune the worker count and concurrency to your hardware.

services:
  postgres:
    image: postgres:16
    restart: unless-stopped
    environment:
      POSTGRES_USER: n8n
      POSTGRES_PASSWORD: change-me-postgres
      POSTGRES_DB: n8n
    volumes:
      - pg_data:/var/lib/postgresql/data
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U n8n -d n8n"]
      interval: 10s
      timeout: 5s
      retries: 5

  redis:
    image: redis:7
    restart: unless-stopped
    command: ["redis-server", "--requirepass", "change-me-redis"]
    volumes:
      - redis_data:/data
    healthcheck:
      test: ["CMD", "redis-cli", "-a", "change-me-redis", "ping"]
      interval: 10s
      timeout: 5s
      retries: 5

  n8n-main:
    image: docker.n8n.io/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: change-me-postgres
      QUEUE_BULL_REDIS_HOST: redis
      QUEUE_BULL_REDIS_PORT: 6379
      QUEUE_BULL_REDIS_PASSWORD: change-me-redis
      N8N_ENCRYPTION_KEY: change-me-shared-encryption-key
      N8N_HOST: n8n.example.com
      WEBHOOK_URL: https://n8n.example.com/
    depends_on:
      postgres:
        condition: service_healthy
      redis:
        condition: service_healthy

  n8n-worker:
    image: docker.n8n.io/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: change-me-postgres
      QUEUE_BULL_REDIS_HOST: redis
      QUEUE_BULL_REDIS_PORT: 6379
      QUEUE_BULL_REDIS_PASSWORD: change-me-redis
      N8N_ENCRYPTION_KEY: change-me-shared-encryption-key
      QUEUE_HEALTH_CHECK_ACTIVE: "true"
    depends_on:
      postgres:
        condition: service_healthy
      redis:
        condition: service_healthy
    deploy:
      replicas: 2

The worker service is the same image with one difference: it starts with the worker command instead of the default. Outside Docker, that command is ./packages/cli/bin/n8n worker from the n8n root directory. The deploy.replicas: 2 line runs two worker containers; with Compose v2 you can also just scale on demand with docker compose up -d --scale n8n-worker=4.

Note that every process repeats the database, Redis, and encryption-key config. That duplication is the point - each process connects independently. The encryption key in particular must be byte-identical across all of them.

Tuning concurrency without killing your database

Set worker concurrency with the --concurrency flag; it defaults to 10 and n8n recommends 5 or higher. Each worker can run that many jobs in parallel:

n8n worker --concurrency=10

The non-obvious trap is the interaction between worker count, concurrency, and your Postgres connection pool. Every concurrently executing job needs database access. If you run many workers each at low concurrency, or many workers at high concurrency, you can exhaust the database connection pool - which shows up as processing delays and failed executions, not as an obvious "out of connections" error. n8n's own guidance is explicit: low concurrency with a large number of workers is the wrong shape. Prefer fewer workers running higher concurrency over a swarm of workers each doing one or two jobs.

Practical starting point: two workers at --concurrency=10 gives you 20 parallel executions, which is plenty for most teams. Scale concurrency up first (it is cheaper - same process, more parallel jobs), and only add worker containers when a single worker's CPU or memory is the bottleneck. Watch Postgres connection count as you grow.

There is also a global ceiling, separate from per-worker concurrency: N8N_CONCURRENCY_PRODUCTION_LIMIT caps total concurrent production executions across the instance and applies in both regular and queue modes (default -1, disabled). Use it as a safety valve when you have a downstream rate limit to respect, not as your primary scaling knob.

Dedicated webhook processors (when to bother)

Add dedicated webhook processes only when webhook ingestion itself is the bottleneck, which is rare. In the default queue setup, the main process receives the webhook, hands the execution to a worker, and the worker runs it. That is already a clean split - the heavy work is off the main process. The main process only struggles with webhooks when you are taking a genuinely large volume of concurrent inbound requests.

When you do hit that wall, you run separate webhook processes:

./packages/cli/bin/n8n webhook

In Docker that is the same image started with the webhook command, and these processes also need EXECUTIONS_MODE=queue, Redis access, and the shared encryption key. You then put a load balancer in front and route /webhook/* and /webhook-waiting/* to the webhook pool, while everything else - the editor, the API, static files, and crucially /webhook-test/* for manual runs - routes to the main process. n8n recommends keeping the main process out of the webhook load-balancer pool entirely, because mixing UI traffic and webhook load on one process degrades the editing experience.

If you want webhook processors to own all production webhooks, disable production webhook handling on main:

export N8N_DISABLE_PRODUCTION_MAIN_PROCESS=true

For most teams this layer is overkill. Start with main plus workers; add webhook processors only when monitoring proves the main process is the request-handling bottleneck.

The gotchas that actually bite

A few things break quietly and waste an afternoon:

  • Mismatched encryption keys. Workers connect, pull jobs, and then fail to decrypt credentials. Pin N8N_ENCRYPTION_KEY everywhere and confirm it matches before debugging anything else.

  • Filesystem binary data in queue mode. Files vanish across processes. Move to S3-compatible external storage.

  • SQLite under queue mode. Unsupported and unstable with multiple processes. Postgres only.

  • Over-sharding workers. Many low-concurrency workers exhaust the Postgres connection pool. Fewer workers, higher concurrency.

  • Forgetting EXECUTIONS_MODE=queue on a worker. A worker started in default mode will not pull from the queue and will sit idle while you wonder why jobs are not draining.

For the full list of variables that govern queue behavior, webhook URL resolution, and the handful that protect you from data loss, keep the complete n8n environment variable reference open in a tab while you build this.

FAQ

Do I need queue mode for a small n8n instance?

No. Regular mode (the default single process) handles a lot of load. Switch to queue mode only when executions queue up behind each other, CPU pins during busy periods, or long-running workflows block others. Adopting it early just adds Redis, extra containers, and distributed-system failure modes you do not need yet.

Does queue mode require Redis and Postgres?

Yes to both. Redis is the message broker that holds the execution queue, and Postgres is the shared database every process reads workflows and credentials from. Queue mode on SQLite is explicitly unsupported, so you must be on Postgres (13+ recommended) before enabling it.

How many workers and what concurrency should I start with?

Two workers at --concurrency=10 (20 parallel executions) is a solid starting point for most teams. Scale concurrency up before adding workers, since more workers at low concurrency can exhaust your database connection pool. Add worker containers only when a single worker's CPU or memory is the real bottleneck.

Why are my workers idle while executions pile up?

Almost always a config mismatch. Confirm EXECUTIONS_MODE=queue is set on the worker (not just the main process), that the worker can reach the same Redis instance, and that it connects to the same Postgres database. If jobs run but credentials fail, the N8N_ENCRYPTION_KEY does not match across processes.

Do I need dedicated webhook processes?

Usually not. The main process already offloads execution to workers, so webhook handling is light unless you take a very high volume of concurrent inbound requests. Add n8n webhook processes behind a load balancer only when monitoring shows the main process is the request bottleneck.

Can I run multiple main processes for high availability?

Yes, via multi-main setup (N8N_MULTI_MAIN_SETUP_ENABLED=true), but it requires a self-hosted Enterprise license, all processes on the same n8n version connected to Postgres and Redis, and a load balancer with sticky sessions. It elects a leader for at-most-once tasks like timers and pruning while followers handle regular traffic.

Get it running once, then scale calmly

Queue mode is not complicated once you see it as three roles sharing two backends: a main that dispatches, workers that execute, Redis that queues, Postgres that stores, and one encryption key gluing it together. Stand up the compose stack above, confirm a workflow runs on a worker (the execution log shows the worker), then scale concurrency and worker count against real monitoring rather than guesses.

If you would rather not own the Redis, the Postgres tuning, and the worker scaling yourself - or you have hit a wall and want a second set of eyes on the topology - that is the kind of thing we help teams sort out. Either way, build it on verified config, not copied-and-hoped env vars.


n8n Queue Mode: Scale With Workers & Redis (2026) | n8nlogic