n8n slow on Docker – high CPU and memory fix for production containers

Docker Performance Tuning Guide

Step by Step Guide for Docker Performance Tuning in n8n Production Workloads


 

Who this is for: DevOps engineers and platform engineers who run n8n in Docker containers and need to lower request latency while increasing concurrent workflow throughput. We cover this in detail in the n8n Performance & Scaling Guide.


Quick Diagnosis

Problem – An n8n Docker container shows high request latency and low throughput under concurrent workflow execution.

One‑liner fix – Adding sensible Docker limits and Node.js flags often drops latency by ≈ 30 %:

docker run -d \
  --cpus="2.5" --memory="3g" \
  --ulimit nofile=65535:65535 \
  -e NODE_OPTIONS="--max-old-space-size=2048 --trace-warnings" \
  -p 5678:5678 n8nio/n8n:latest

Run a load test after applying the checklist to verify improvement:

ab -n 100 -c 20 http://localhost:5678/

1. Choose the Right Docker Storage Driver for n8n

If you encounter any cpu profiling resolve them before continuing with the setup.

Why it matters – The storage driver determines how image layers and container filesystems are handled, directly affecting I/O latency.

Storage Driver Pros for n8n Cons / Caveats Recommended Settings
overlay2 (default) Fast copy‑on‑write, low overhead Requires kernel ≥ 4.0; SELinux/AppArmor may need tweaks `–storage-driver=overlay2` in daemon.json
btrfs Built‑in snapshots, good for dev Higher CPU usage, less mature on some distros Use only on a dedicated btrfs volume
devicemapper (direct‑lvm) Predictable performance on block devices Complex setup, slower metadata ops Not recommended for production n8n

EEFA note – On Ubuntu 22.04 the overlay2 driver can hit “slow overlayfs unmount” errors when the host runs out of inodes. Keep /var/lib/docker on a filesystem with ample inode density (e.g., ext4 with -i 4096).

Configure the daemon

Create or edit /etc/docker/daemon.json:

{
  "storage-driver": "overlay2",
  "log-driver": "json-file",
  "log-opts": {
    "max-size": "20m",
    "max-file": "5"
  }
}

Restart Docker to apply the change:

systemctl restart docker

2. CPU & Memory Cgroup Tuning

2.1 Allocate fractional CPUs

Node.js benefits from dedicated cores; over‑committing leads to context‑switch thrash.

docker run -d \
  --cpus="3.5" \
  --cpu-shares=1024 \
  --memory="4g" \
  n8nio/n8n:latest

Why 3.5? Empirically, 1 vCPU per ≈ 1.2 concurrent workflow executions yields the best latency‑throughput ratio for n8n’s default worker pool.

2.2 Fine‑grained throttling with cpu-period / cpu-quota

docker run -d \
  --cpu-period=100000 \
  --cpu-quota=350000 \
  n8nio/n8n:latest

EEFA warning – Setting cpu-quota higher than the host’s physical cores can cause OOM kills on busy hosts. Pair with a memory reservation to avoid cascading failures. If you encounter any resource limiting with cgroups resolve them before continuing with the setup.

2.3 Align memory limits with V8 heap

docker run -d \
  --memory="4g" \
  -e NODE_OPTIONS="--max-old-space-size=3072" \
  n8nio/n8n:latest

Disable swap for production stability:

docker run -d \
  --memory-swap=0 \
  n8nio/n8n:latest

3. Network Stack Optimizations

3.1 Host networking for low‑latency intra‑cluster traffic

When n8n talks heavily to internal services (PostgreSQL, Redis, external APIs), the default bridge NAT adds ~1‑2 ms per request.

docker run -d \
  --network host \
  n8nio/n8n:latest

EEFA note – Host mode bypasses Docker isolation; ensure the host firewall restricts inbound traffic to port 5678.

3.2 Tune MTU and enable BBR congestion control

Create a bridge with a matching MTU:

docker network create \
  --driver bridge \
  --opt com.docker.network.driver.mtu=1500 \
  n8n_bridge

Run the container on the custom bridge:

docker run -d \
  --network n8n_bridge \
  n8nio/n8n:latest

Enable BBR for lower latency (Linux ≥ 5.4):

sysctl -w net.ipv4.tcp_congestion_control=bbr

Persist the setting:

echo "net.ipv4.tcp_congestion_control=bbr" > /etc/sysctl.d/99-n8n.conf

4. Persistent Volume Performance

n8n stores workflow JSON and execution logs at /home/node/.n8n. The I/O path directly impacts throughput.

Volume Type Latency (ms) Throughput (MB/s) Recommended Use
bind mount (host dir) 0.8 150 Quick dev, low contention
named volume (local driver) 1.2 120 Production, automatic cleanup
tmpfs (in‑memory) 0.2 300+ High‑speed cache, non‑persistent
block device (direct‑lvm) 0.5 250 Heavy write loads, SSD only

Use a dedicated SSD block device in production

Create the volume:

docker volume create \
  --driver local \
  --opt type=block \
  --opt device=/dev/disk/by-id/ssd-n8n \
  --opt o=rw \
  n8n_data

Mount it when starting the container:

docker run -d \
  -v n8n_data:/home/node/.n8n \
  n8nio/n8n:latest

EEFA note – Mount the block device with noatime to avoid extra metadata writes (add -o noatime in /etc/fstab). If you encounter any custom node performance resolve them before continuing with the setup.


5. Node.js Runtime Flags for Low‑Latency Execution

Fine‑tune V8 to match Docker limits.

Flag Effect Recommended Value
–max-old-space-size Caps heap size, prevents OOM 2048 – 3072 MiB (match Docker memory)
–optimize-for-size Reduces code size, slightly slower start Off for latency‑critical
–trace-warnings Emits warnings for deprecated APIs On in staging
–tls-min-v1.2 Enforces modern TLS, reduces handshake overhead On if external APIs support it
–no-expose-gc Disables manual GC triggers, lets V8 manage On for steady‑state workloads

Pass the flags via NODE_OPTIONS:

docker run -d \
  -e NODE_OPTIONS="--max-old-space-size=2560 --trace-warnings --tls-min-v1.2" \
  n8nio/n8n:latest

6. Automated Health‑Check & Auto‑Restart

Docker health‑checks catch latency spikes before they cascade.

Add a health‑check to the image (Dockerfile snippet):

HEALTHCHECK --interval=30s --timeout=5s \
  CMD curl -f http://localhost:5678/healthz || exit 1

Run the container with a restart policy that respects health status:

docker run -d \
  --restart on-failure:5 \
  --health-cmd="curl -f http://localhost:5678/healthz || exit 1" \
  n8nio/n8n:latest

EEFA tip – Pair with a sidecar Prometheus exporter (n8n-prometheus-exporter) to visualize request latency and trigger alerts when container_cpu_user_seconds_total exceeds a threshold.


7. Step‑by‑Step Tuning Checklist

Steps Action
1 Set Docker daemon storage driver to overlay2 (update /etc/docker/daemon.json).
2 Allocate ≥ 3 vCPU and ≥ 4 GiB RAM to the container (--cpus, --memory).
3 Align NODE_OPTIONS --max-old-space-size with Docker memory limit.
4 Use host networking or a tuned bridge with MTU = 1500.
5 Mount a **dedicated SSD block device** as a Docker volume for /home/node/.n8n.
6 Enable a **Docker health‑check** that hits /healthz.
7 Apply **BBR** congestion control on the host (net.ipv4.tcp_congestion_control=bbr).
8 Enforce a no‑swap policy (--memory-swap=0) in production.
9 Restart the container, run a load test (ab, hey, or wrk) and record 95th‑percentile latency.
10 Iterate: adjust --cpu-quota or add more memory if latency > 200 ms under target load.

8. Troubleshooting Common Bottlenecks

Symptom Likely Cause Fix
Latency spikes > 500 ms under load CPU throttling (cpu-quota too low) Increase --cpu-quota or add more physical cores.
“container OOMKilled” in logs Heap size exceeds memory limit Raise --max-old-space-size and/or --memory.
“Failed to mount volume” errors Incorrect block device permissions chown 1000:1000 /dev/disk/by-id/ssd-n8n or use --userns-remap.
Persistent 2‑3 ms per request overhead Bridge NAT latency Switch to --network host or tune MTU.
“Too many open files” error ulimit too low Add --ulimit nofile=65535:65535 to docker run.

 


Takeaway

Align Docker’s cgroup limits, storage driver, networking mode, and Node.js runtime flags with n8n’s execution model to shave tens of milliseconds off each request and sustain higher concurrent workflow counts without sacrificing stability. Apply the checklist, monitor with the built‑in health‑check, and iterate based on real‑world load metrics.

Leave a Comment

Your email address will not be published. Required fields are marked *