<p><img class="aligncenter size-full wp-image-4782" src="https://flowgenius.in/wp-content/uploads/2026/01/docker-performance-tuning.png" alt="Docker Performance Tuning Guide" /></p>
<p style="text-align: center;">Step by Step Guide for Docker Performance Tuning in n8n Production Workloads</p>
<hr />
<p> </p>
<p style="margin-bottom: 2em; line-height: 1.9;"><strong>Who this is for:</strong> DevOps engineers and platform engineers who run n8n in Docker containers and need to lower request latency while increasing concurrent workflow throughput. <strong>We cover this in detail in the </strong><a href="https://flowgenius.in/n8n-performance-and-scaling-guide/">n8n Performance & Scaling Guide.</a></p>
<hr style="margin: 60px 0; border: none; border-top: 1px solid #eee;" />
<h2 style="margin-bottom: 45px; line-height: 1.3;">Quick Diagnosis</h2>
<p style="margin-bottom: 2em; line-height: 1.9;"><strong>Problem</strong> – An n8n Docker container shows high request latency and low throughput under concurrent workflow execution.</p>
<p style="margin-bottom: 2em; line-height: 1.9;"><strong>One‑liner fix</strong> – Adding sensible Docker limits and Node.js flags often drops latency by ≈ 30 %:</p>
<pre style="background: #fafafa; padding: 20px; border: 1px solid #e0e0e0; overflow: auto;">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</pre>
<p style="margin-bottom: 2em; line-height: 1.9;">Run a load test after applying the checklist to verify improvement:</p>
<pre style="background: #fafafa; padding: 20px; border: 1px solid #e0e0e0; overflow: auto;">ab -n 100 -c 20 http://localhost:5678/</pre>
<hr style="margin: 60px 0; border: none; border-top: 1px solid #eee;" />
<h2 style="margin-bottom: 45px; line-height: 1.3;">1. Choose the Right Docker Storage Driver for n8n</h2>
<p><strong>If you encounter any </strong><a href="/cpu-profiling">cpu profiling </a><strong>resolve them before continuing with the setup.</strong></p>
<p style="margin-bottom: 2em; line-height: 1.9;"><strong>Why it matters</strong> – The storage driver determines how image layers and container filesystems are handled, directly affecting I/O latency.</p>
<table style="border-collapse: collapse; width: 100%; margin-bottom: 2em;">
<thead>
<tr>
<th style="border: 1px solid #e0e0e0; padding: 13px;">Storage Driver</th>
<th style="border: 1px solid #e0e0e0; padding: 13px;">Pros for n8n</th>
<th style="border: 1px solid #e0e0e0; padding: 13px;">Cons / Caveats</th>
<th style="border: 1px solid #e0e0e0; padding: 13px;">Recommended Settings</th>
</tr>
</thead>
<tbody>
<tr>
<td style="border: 1px solid #e0e0e0; padding: 13px;"><strong>overlay2</strong> (default)</td>
<td style="border: 1px solid #e0e0e0; padding: 13px;">Fast copy‑on‑write, low overhead</td>
<td style="border: 1px solid #e0e0e0; padding: 13px;">Requires kernel ≥ 4.0; SELinux/AppArmor may need tweaks</td>
<td style="border: 1px solid #e0e0e0; padding: 13px;">`–storage-driver=overlay2` in <code>daemon.json</code></td>
</tr>
<tr>
<td style="border: 1px solid #e0e0e0; padding: 13px;"><strong>btrfs</strong></td>
<td style="border: 1px solid #e0e0e0; padding: 13px;">Built‑in snapshots, good for dev</td>
<td style="border: 1px solid #e0e0e0; padding: 13px;">Higher CPU usage, less mature on some distros</td>
<td style="border: 1px solid #e0e0e0; padding: 13px;">Use only on a dedicated btrfs volume</td>
</tr>
<tr>
<td style="border: 1px solid #e0e0e0; padding: 13px;"><strong>devicemapper (direct‑lvm)</strong></td>
<td style="border: 1px solid #e0e0e0; padding: 13px;">Predictable performance on block devices</td>
<td style="border: 1px solid #e0e0e0; padding: 13px;">Complex setup, slower metadata ops</td>
<td style="border: 1px solid #e0e0e0; padding: 13px;">Not recommended for production n8n</td>
</tr>
</tbody>
</table>
<p style="margin-bottom: 2em; line-height: 1.9;"><strong>EEFA note</strong> – On Ubuntu 22.04 the overlay2 driver can hit “slow overlayfs unmount” errors when the host runs out of inodes. Keep <code>/var/lib/docker</code> on a filesystem with ample inode density (e.g., ext4 with <code>-i 4096</code>).</p>
<h3 style="margin-bottom: 45px; line-height: 1.3;">Configure the daemon</h3>
<p style="margin-bottom: 2em; line-height: 1.9;">Create or edit <code>/etc/docker/daemon.json</code>:</p>
<pre style="background: #fafafa; padding: 20px; border: 1px solid #e0e0e0; overflow: auto;">{
"storage-driver": "overlay2",
"log-driver": "json-file",
"log-opts": {
"max-size": "20m",
"max-file": "5"
}
}</pre>
<p style="margin-bottom: 2em; line-height: 1.9;">Restart Docker to apply the change:</p>
<pre style="background: #fafafa; padding: 20px; border: 1px solid #e0e0e0; overflow: auto;">systemctl restart docker</pre>
<hr style="margin: 60px 0; border: none; border-top: 1px solid #eee;" />
<h2 style="margin-bottom: 45px; line-height: 1.3;">2. CPU & Memory Cgroup Tuning</h2>
<h3 style="margin-bottom: 45px; line-height: 1.3;">2.1 Allocate fractional CPUs</h3>
<p style="margin-bottom: 2em; line-height: 1.9;">Node.js benefits from dedicated cores; over‑committing leads to context‑switch thrash.</p>
<pre style="background: #fafafa; padding: 20px; border: 1px solid #e0e0e0; overflow: auto;">docker run -d \
--cpus="3.5" \
--cpu-shares=1024 \
--memory="4g" \
n8nio/n8n:latest</pre>
<p style="margin-bottom: 2em; line-height: 1.9;"><em>Why 3.5?</em> Empirically, 1 vCPU per ≈ 1.2 concurrent workflow executions yields the best latency‑throughput ratio for n8n’s default worker pool.</p>
<h3 style="margin-bottom: 45px; line-height: 1.3;">2.2 Fine‑grained throttling with <code>cpu-period</code> / <code>cpu-quota</code></h3>
<pre style="background: #fafafa; padding: 20px; border: 1px solid #e0e0e0; overflow: auto;">docker run -d \
--cpu-period=100000 \
--cpu-quota=350000 \
n8nio/n8n:latest</pre>
<p style="margin-bottom: 2em; line-height: 1.9;"><strong>EEFA warning</strong> – Setting <code>cpu-quota</code> 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 <a href="/resource-limiting-with-cgroups">resource limiting with cgroups </a>resolve them before continuing with the setup.</p>
<h3 style="margin-bottom: 45px; line-height: 1.3;">2.3 Align memory limits with V8 heap</h3>
<pre style="background: #fafafa; padding: 20px; border: 1px solid #e0e0e0; overflow: auto;">docker run -d \
--memory="4g" \
-e NODE_OPTIONS="--max-old-space-size=3072" \
n8nio/n8n:latest</pre>
<p style="margin-bottom: 2em; line-height: 1.9;">Disable swap for production stability:</p>
<pre style="background: #fafafa; padding: 20px; border: 1px solid #e0e0e0; overflow: auto;">docker run -d \
--memory-swap=0 \
n8nio/n8n:latest</pre>
<hr style="margin: 60px 0; border: none; border-top: 1px solid #eee;" />
<h2 style="margin-bottom: 45px; line-height: 1.3;">3. Network Stack Optimizations</h2>
<h3 style="margin-bottom: 45px; line-height: 1.3;">3.1 Host networking for low‑latency intra‑cluster traffic</h3>
<p style="margin-bottom: 2em; line-height: 1.9;">When n8n talks heavily to internal services (PostgreSQL, Redis, external APIs), the default bridge NAT adds ~1‑2 ms per request.</p>
<pre style="background: #fafafa; padding: 20px; border: 1px solid #e0e0e0; overflow: auto;">docker run -d \
--network host \
n8nio/n8n:latest</pre>
<p style="margin-bottom: 2em; line-height: 1.9;"><strong>EEFA note</strong> – Host mode bypasses Docker isolation; ensure the host firewall restricts inbound traffic to port 5678.</p>
<h3 style="margin-bottom: 45px; line-height: 1.3;">3.2 Tune MTU and enable BBR congestion control</h3>
<p style="margin-bottom: 2em; line-height: 1.9;">Create a bridge with a matching MTU:</p>
<pre style="background: #fafafa; padding: 20px; border: 1px solid #e0e0e0; overflow: auto;">docker network create \
--driver bridge \
--opt com.docker.network.driver.mtu=1500 \
n8n_bridge</pre>
<p style="margin-bottom: 2em; line-height: 1.9;">Run the container on the custom bridge:</p>
<pre style="background: #fafafa; padding: 20px; border: 1px solid #e0e0e0; overflow: auto;">docker run -d \
--network n8n_bridge \
n8nio/n8n:latest</pre>
<p style="margin-bottom: 2em; line-height: 1.9;">Enable BBR for lower latency (Linux ≥ 5.4):</p>
<pre style="background: #fafafa; padding: 20px; border: 1px solid #e0e0e0; overflow: auto;">sysctl -w net.ipv4.tcp_congestion_control=bbr</pre>
<p style="margin-bottom: 2em; line-height: 1.9;">Persist the setting:</p>
<pre style="background: #fafafa; padding: 20px; border: 1px solid #e0e0e0; overflow: auto;">echo "net.ipv4.tcp_congestion_control=bbr" > /etc/sysctl.d/99-n8n.conf</pre>
<hr style="margin: 60px 0; border: none; border-top: 1px solid #eee;" />
<h2 style="margin-bottom: 45px; line-height: 1.3;">4. Persistent Volume Performance</h2>
<p style="margin-bottom: 2em; line-height: 1.9;">n8n stores workflow JSON and execution logs at <code>/home/node/.n8n</code>. The I/O path directly impacts throughput.</p>
<table style="border-collapse: collapse; width: 100%; margin-bottom: 2em;">
<thead>
<tr>
<th style="border: 1px solid #e0e0e0; padding: 13px;">Volume Type</th>
<th style="border: 1px solid #e0e0e0; padding: 13px;">Latency (ms)</th>
<th style="border: 1px solid #e0e0e0; padding: 13px;">Throughput (MB/s)</th>
<th style="border: 1px solid #e0e0e0; padding: 13px;">Recommended Use</th>
</tr>
</thead>
<tbody>
<tr>
<td style="border: 1px solid #e0e0e0; padding: 13px;"><strong>bind mount</strong> (host dir)</td>
<td style="border: 1px solid #e0e0e0; padding: 13px;">0.8</td>
<td style="border: 1px solid #e0e0e0; padding: 13px;">150</td>
<td style="border: 1px solid #e0e0e0; padding: 13px;">Quick dev, low contention</td>
</tr>
<tr>
<td style="border: 1px solid #e0e0e0; padding: 13px;"><strong>named volume</strong> (local driver)</td>
<td style="border: 1px solid #e0e0e0; padding: 13px;">1.2</td>
<td style="border: 1px solid #e0e0e0; padding: 13px;">120</td>
<td style="border: 1px solid #e0e0e0; padding: 13px;">Production, automatic cleanup</td>
</tr>
<tr>
<td style="border: 1px solid #e0e0e0; padding: 13px;"><strong>tmpfs</strong> (in‑memory)</td>
<td style="border: 1px solid #e0e0e0; padding: 13px;">0.2</td>
<td style="border: 1px solid #e0e0e0; padding: 13px;">300+</td>
<td style="border: 1px solid #e0e0e0; padding: 13px;">High‑speed cache, non‑persistent</td>
</tr>
<tr>
<td style="border: 1px solid #e0e0e0; padding: 13px;"><strong>block device</strong> (direct‑lvm)</td>
<td style="border: 1px solid #e0e0e0; padding: 13px;">0.5</td>
<td style="border: 1px solid #e0e0e0; padding: 13px;">250</td>
<td style="border: 1px solid #e0e0e0; padding: 13px;">Heavy write loads, SSD only</td>
</tr>
</tbody>
</table>
<h3 style="margin-bottom: 45px; line-height: 1.3;">Use a dedicated SSD block device in production</h3>
<p style="margin-bottom: 2em; line-height: 1.9;">Create the volume:</p>
<pre style="background: #fafafa; padding: 20px; border: 1px solid #e0e0e0; overflow: auto;">docker volume create \
--driver local \
--opt type=block \
--opt device=/dev/disk/by-id/ssd-n8n \
--opt o=rw \
n8n_data</pre>
<p style="margin-bottom: 2em; line-height: 1.9;">Mount it when starting the container:</p>
<pre style="background: #fafafa; padding: 20px; border: 1px solid #e0e0e0; overflow: auto;">docker run -d \
-v n8n_data:/home/node/.n8n \
n8nio/n8n:latest</pre>
<p style="margin-bottom: 2em; line-height: 1.9;"><strong>EEFA note</strong> – Mount the block device with <code>noatime</code> to avoid extra metadata writes (add <code>-o noatime</code> in <code>/etc/fstab</code>). If you encounter any <a href="/custom-node-performance">custom node performance </a>resolve them before continuing with the setup.</p>
<hr style="margin: 60px 0; border: none; border-top: 1px solid #eee;" />
<h2 style="margin-bottom: 45px; line-height: 1.3;">5. Node.js Runtime Flags for Low‑Latency Execution</h2>
<p style="margin-bottom: 2em; line-height: 1.9;">Fine‑tune V8 to match Docker limits.</p>
<table style="border-collapse: collapse; width: 100%; margin-bottom: 2em;">
<thead>
<tr>
<th style="border: 1px solid #e0e0e0; padding: 13px;">Flag</th>
<th style="border: 1px solid #e0e0e0; padding: 13px;">Effect</th>
<th style="border: 1px solid #e0e0e0; padding: 13px;">Recommended Value</th>
</tr>
</thead>
<tbody>
<tr>
<td style="border: 1px solid #e0e0e0; padding: 13px;">–max-old-space-size</td>
<td style="border: 1px solid #e0e0e0; padding: 13px;">Caps heap size, prevents OOM</td>
<td style="border: 1px solid #e0e0e0; padding: 13px;">2048 – 3072 MiB (match Docker memory)</td>
</tr>
<tr>
<td style="border: 1px solid #e0e0e0; padding: 13px;">–optimize-for-size</td>
<td style="border: 1px solid #e0e0e0; padding: 13px;">Reduces code size, slightly slower start</td>
<td style="border: 1px solid #e0e0e0; padding: 13px;"><strong>Off</strong> for latency‑critical</td>
</tr>
<tr>
<td style="border: 1px solid #e0e0e0; padding: 13px;">–trace-warnings</td>
<td style="border: 1px solid #e0e0e0; padding: 13px;">Emits warnings for deprecated APIs</td>
<td style="border: 1px solid #e0e0e0; padding: 13px;"><strong>On</strong> in staging</td>
</tr>
<tr>
<td style="border: 1px solid #e0e0e0; padding: 13px;">–tls-min-v1.2</td>
<td style="border: 1px solid #e0e0e0; padding: 13px;">Enforces modern TLS, reduces handshake overhead</td>
<td style="border: 1px solid #e0e0e0; padding: 13px;"><strong>On</strong> if external APIs support it</td>
</tr>
<tr>
<td style="border: 1px solid #e0e0e0; padding: 13px;">–no-expose-gc</td>
<td style="border: 1px solid #e0e0e0; padding: 13px;">Disables manual GC triggers, lets V8 manage</td>
<td style="border: 1px solid #e0e0e0; padding: 13px;"><strong>On</strong> for steady‑state workloads</td>
</tr>
</tbody>
</table>
<p style="margin-bottom: 2em; line-height: 1.9;">Pass the flags via <code>NODE_OPTIONS</code>:</p>
<pre style="background: #fafafa; padding: 20px; border: 1px solid #e0e0e0; overflow: auto;">docker run -d \
-e NODE_OPTIONS="--max-old-space-size=2560 --trace-warnings --tls-min-v1.2" \
n8nio/n8n:latest</pre>
<hr style="margin: 60px 0; border: none; border-top: 1px solid #eee;" />
<h2 style="margin-bottom: 45px; line-height: 1.3;">6. Automated Health‑Check & Auto‑Restart</h2>
<p style="margin-bottom: 2em; line-height: 1.9;">Docker health‑checks catch latency spikes before they cascade.</p>
<p style="margin-bottom: 2em; line-height: 1.9;">Add a health‑check to the image (Dockerfile snippet):</p>
<pre style="background: #fafafa; padding: 20px; border: 1px solid #e0e0e0; overflow: auto;">HEALTHCHECK --interval=30s --timeout=5s \
CMD curl -f http://localhost:5678/healthz || exit 1</pre>
<p style="margin-bottom: 2em; line-height: 1.9;">Run the container with a restart policy that respects health status:</p>
<pre style="background: #fafafa; padding: 20px; border: 1px solid #e0e0e0; overflow: auto;">docker run -d \
--restart on-failure:5 \
--health-cmd="curl -f http://localhost:5678/healthz || exit 1" \
n8nio/n8n:latest</pre>
<p style="margin-bottom: 2em; line-height: 1.9;"><strong>EEFA tip</strong> – Pair with a sidecar Prometheus exporter (<code>n8n-prometheus-exporter</code>) to visualize request latency and trigger alerts when <code>container_cpu_user_seconds_total</code> exceeds a threshold.</p>
<hr style="margin: 60px 0; border: none; border-top: 1px solid #eee;" />
<h2 style="margin-bottom: 45px; line-height: 1.3;">7. Step‑by‑Step Tuning Checklist</h2>
<table style="border-collapse: collapse; width: 100%; margin-bottom: 2em;">
<thead>
<tr>
<th style="border: 1px solid #e0e0e0; padding: 13px;">Steps</th>
<th style="border: 1px solid #e0e0e0; padding: 13px;">Action</th>
</tr>
</thead>
<tbody>
<tr>
<td style="border: 1px solid #e0e0e0; padding: 13px;">1</td>
<td style="border: 1px solid #e0e0e0; padding: 13px;">Set Docker daemon storage driver to <strong>overlay2</strong> (update <code>/etc/docker/daemon.json</code>).</td>
</tr>
<tr>
<td style="border: 1px solid #e0e0e0; padding: 13px;">2</td>
<td style="border: 1px solid #e0e0e0; padding: 13px;">Allocate <strong>≥ 3 vCPU</strong> and <strong>≥ 4 GiB</strong> RAM to the container (<code>--cpus</code>, <code>--memory</code>).</td>
</tr>
<tr>
<td style="border: 1px solid #e0e0e0; padding: 13px;">3</td>
<td style="border: 1px solid #e0e0e0; padding: 13px;">Align <code>NODE_OPTIONS</code> <code>--max-old-space-size</code> with Docker memory limit.</td>
</tr>
<tr>
<td style="border: 1px solid #e0e0e0; padding: 13px;">4</td>
<td style="border: 1px solid #e0e0e0; padding: 13px;">Use <strong>host networking</strong> or a tuned bridge with MTU = 1500.</td>
</tr>
<tr>
<td style="border: 1px solid #e0e0e0; padding: 13px;">5</td>
<td style="border: 1px solid #e0e0e0; padding: 13px;">Mount a **dedicated SSD block device** as a Docker volume for <code>/home/node/.n8n</code>.</td>
</tr>
<tr>
<td style="border: 1px solid #e0e0e0; padding: 13px;">6</td>
<td style="border: 1px solid #e0e0e0; padding: 13px;">Enable a **Docker health‑check** that hits <code>/healthz</code>.</td>
</tr>
<tr>
<td style="border: 1px solid #e0e0e0; padding: 13px;">7</td>
<td style="border: 1px solid #e0e0e0; padding: 13px;">Apply **BBR** congestion control on the host (<code>net.ipv4.tcp_congestion_control=bbr</code>).</td>
</tr>
<tr>
<td style="border: 1px solid #e0e0e0; padding: 13px;">8</td>
<td style="border: 1px solid #e0e0e0; padding: 13px;">Enforce a no‑swap policy (<code>--memory-swap=0</code>) in production.</td>
</tr>
<tr>
<td style="border: 1px solid #e0e0e0; padding: 13px;">9</td>
<td style="border: 1px solid #e0e0e0; padding: 13px;">Restart the container, run a load test (<code>ab</code>, <code>hey</code>, or <code>wrk</code>) and record 95th‑percentile latency.</td>
</tr>
<tr>
<td style="border: 1px solid #e0e0e0; padding: 13px;">10</td>
<td style="border: 1px solid #e0e0e0; padding: 13px;">Iterate: adjust <code>--cpu-quota</code> or add more memory if latency > 200 ms under target load.</td>
</tr>
</tbody>
</table>
<hr style="margin: 60px 0; border: none; border-top: 1px solid #eee;" />
<h2 style="margin-bottom: 45px; line-height: 1.3;">8. Troubleshooting Common Bottlenecks</h2>
<table style="border-collapse: collapse; width: 100%; margin-bottom: 2em;">
<thead>
<tr>
<th style="border: 1px solid #e0e0e0; padding: 13px;">Symptom</th>
<th style="border: 1px solid #e0e0e0; padding: 13px;">Likely Cause</th>
<th style="border: 1px solid #e0e0e0; padding: 13px;">Fix</th>
</tr>
</thead>
<tbody>
<tr>
<td style="border: 1px solid #e0e0e0; padding: 13px;">Latency spikes > 500 ms under load</td>
<td style="border: 1px solid #e0e0e0; padding: 13px;">CPU throttling (<code>cpu-quota</code> too low)</td>
<td style="border: 1px solid #e0e0e0; padding: 13px;">Increase <code>--cpu-quota</code> or add more physical cores.</td>
</tr>
<tr>
<td style="border: 1px solid #e0e0e0; padding: 13px;">“container OOMKilled” in logs</td>
<td style="border: 1px solid #e0e0e0; padding: 13px;">Heap size exceeds memory limit</td>
<td style="border: 1px solid #e0e0e0; padding: 13px;">Raise <code>--max-old-space-size</code> and/or <code>--memory</code>.</td>
</tr>
<tr>
<td style="border: 1px solid #e0e0e0; padding: 13px;">“Failed to mount volume” errors</td>
<td style="border: 1px solid #e0e0e0; padding: 13px;">Incorrect block device permissions</td>
<td style="border: 1px solid #e0e0e0; padding: 13px;"><code>chown 1000:1000 /dev/disk/by-id/ssd-n8n</code> or use <code>--userns-remap</code>.</td>
</tr>
<tr>
<td style="border: 1px solid #e0e0e0; padding: 13px;">Persistent 2‑3 ms per request overhead</td>
<td style="border: 1px solid #e0e0e0; padding: 13px;">Bridge NAT latency</td>
<td style="border: 1px solid #e0e0e0; padding: 13px;">Switch to <code>--network host</code> or tune MTU.</td>
</tr>
<tr>
<td style="border: 1px solid #e0e0e0; padding: 13px;">“Too many open files” error</td>
<td style="border: 1px solid #e0e0e0; padding: 13px;">ulimit too low</td>
<td style="border: 1px solid #e0e0e0; padding: 13px;">Add <code>--ulimit nofile=65535:65535</code> to <code>docker run</code>.</td>
</tr>
</tbody>
</table>
<p> </p>
<hr style="margin: 60px 0; border: none; border-top: 1px solid #eee;" />
<h3 style="margin-bottom: 45px; line-height: 1.3;">Takeaway</h3>
<p style="margin-bottom: 2em; line-height: 1.9;">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.</p>

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.