<p class="wp-block-image aligncenter"><img src="https://flowgenius.in/wp-content/uploads/2026/01/benchmarking-tools.png" alt="Step by Step Guide to solve benchmarking tools" /></p>
<p> </p>
<p class="wp-block-image aligncenter">Step by Step Guide to solve benchmarking tools</p>
<hr />
<p style="margin-bottom: 45px; line-height: 1.3;"><strong>Who this is for</strong>: Developers and SREs who need a reproducible way to measure n8n workflow throughput and latency in a CI‑ready, production‑like environment. <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>
<div style="margin: 50px 0;">
<hr />
</div>
<h2 style="margin-bottom: 45px; line-height: 1.3;">Quick Diagnosis</h2>
<p style="margin-bottom: 2em; line-height: 1.9;"><strong>Problem:</strong> You need a reproducible way to measure n8n workflow throughput and latency.</p>
<p style="margin-bottom: 2em; line-height: 1.9;"><strong>Solution:</strong> Deploy a lightweight load generator (k6 or Locust), target the n8n REST API (<code>/webhook/...</code> or <code>/executions</code>), run a scripted scenario that mimics real‑world payloads, and capture VU‑seconds, response‑time percentiles, and error rates. Export the results to CSV/JSON for analysis or CI integration. If you encounter any <a href="/monitoring-dashboard-setup">monitoring dashboard setup </a>resolve them before continuing with the setup.</p>
<div style="margin: 50px 0;">
<hr />
</div>
<h2 style="margin-bottom: 45px; line-height: 1.3;">1. Choosing the Right Load Generator for n8n</h2>
<table style="border-collapse: collapse; width: 100%; margin-bottom: 2em;">
<thead>
<tr>
<th style="border: 1px solid #ddd; padding: 13px;">Feature</th>
<th style="border: 1px solid #ddd; padding: 13px;">k6</th>
<th style="border: 1px solid #ddd; padding: 13px;">Locust</th>
<th style="border: 1px solid #ddd; padding: 13px;">When to Prefer</th>
</tr>
</thead>
<tbody>
<tr>
<td style="border: 1px solid #ddd; padding: 13px;">Language</td>
<td style="border: 1px solid #ddd; padding: 13px;">JavaScript (ES6)</td>
<td style="border: 1px solid #ddd; padding: 13px;">Python</td>
<td style="border: 1px solid #ddd; padding: 13px;">Use k6 if your team is JS‑centric; Locust if you need complex stateful user flows.</td>
</tr>
<tr>
<td style="border: 1px solid #ddd; padding: 13px;">CLI‑only vs UI</td>
<td style="border: 1px solid #ddd; padding: 13px;">CLI only (HTML report optional)</td>
<td style="border: 1px solid #ddd; padding: 13px;">Web UI + CLI</td>
<td style="border: 1px solid #ddd; padding: 13px;">Locust’s UI is handy for exploratory testing.</td>
</tr>
<tr>
<td style="border: 1px solid #ddd; padding: 13px;">Distributed Load</td>
<td style="border: 1px solid #ddd; padding: 13px;">Built‑in cloud/remote workers</td>
<td style="border: 1px solid #ddd; padding: 13px;">Master‑worker architecture</td>
<td style="border: 1px solid #ddd; padding: 13px;">For > 10k VUs, both scale, but k6’s cloud service offers managed scaling.</td>
</tr>
<tr>
<td style="border: 1px solid #ddd; padding: 13px;">Integration</td>
<td style="border: 1px solid #ddd; padding: 13px;">InfluxDB, Grafana, CI pipelines</td>
<td style="border: 1px solid #ddd; padding: 13px;">Prometheus, Grafana, CI pipelines</td>
<td style="border: 1px solid #ddd; padding: 13px;">Choose based on existing observability stack.</td>
</tr>
<tr>
<td style="border: 1px solid #ddd; padding: 13px;">License</td>
<td style="border: 1px solid #ddd; padding: 13px;">Open‑source (MIT) + commercial cloud</td>
<td style="border: 1px solid #ddd; padding: 13px;">Open‑source (MIT)</td>
<td style="border: 1px solid #ddd; padding: 13px;">Both free; pick the one that matches your tech stack.</td>
</tr>
</tbody>
</table>
<p style="margin-bottom: 2em; line-height: 1.9;"><em>EEFA note:</em> Both tools generate HTTP traffic only. To benchmark n8n’s internal queue processing, combine the load test with a background worker monitor (see “Measuring Queue Drain Rate” later). If you encounter any <a href="/logging-optimization">logging optimization </a>resolve them before continuing with the setup.</p>
<div style="margin: 50px 0;">
<hr />
</div>
<h2 style="margin-bottom: 45px; line-height: 1.3;">2. Preparing n8n for Benchmarking</h2>
<p style="margin-bottom: 2em; line-height: 1.9;"><strong>Micro‑summary:</strong> Set up an isolated n8n instance with a minimal echo workflow and deterministic logging to ensure the benchmark measures HTTP handling, not background processing.</p>
<h3 style="margin-bottom: 45px; line-height: 1.3;">2.1 Spin up a dedicated n8n instance</h3>
<pre style="background: #fafafa; padding: 20px; border: 1px solid #e0e0e0; margin-bottom: 2em; line-height: 1.9;"># docker-compose.yml – n8n service
services:
n8n:
image: n8nio/n8n:latest
environment:
- LOG_LEVEL=debug
- EXECUTIONS_PROCESS=1
- N8N_BASIC_AUTH_ACTIVE=true
- N8N_BASIC_AUTH_USER=bench
- N8N_BASIC_AUTH_PASSWORD=bench123
ports:
- "5678:5678"
</pre>
<h3 style="margin-bottom: 45px; line-height: 1.3;">2.2 Create a simple “echo” webhook workflow</h3>
<ol style="margin-bottom: 1.5em; line-height: 1.9;">
<li>Add a <strong>Webhook</strong> node listening on <code>/benchmark</code>.</li>
<li>Connect it directly to a <strong>Set</strong> node that returns <code>{{ $json }}</code>.</li>
<li>Deploy the workflow.</li>
</ol>
<p style="margin-bottom: 2em; line-height: 1.9;"><em>EEFA warning:</em> Do <strong>not</strong> run benchmarks on a multi‑node cluster without synchronizing <code>EXECUTIONS_PROCESS</code> across pods; otherwise, results will reflect load‑balancer jitter rather than true per‑node capacity. If you encounter any <a href="/security-impact-on-performance">security impact on performance </a>resolve them before continuing with the setup.</p>
<div style="margin: 50px 0;">
<hr />
</div>
<h2 style="margin-bottom: 45px; line-height: 1.3;">3. k6 Benchmark Script for n8n Webhook</h2>
<p style="margin-bottom: 2em; line-height: 1.9;"><strong>Micro‑summary:</strong> Install k6, write a short script that posts JSON payloads, and capture custom latency and error metrics.</p>
<h3 style="margin-bottom: 45px; line-height: 1.3;">3.1 Install k6</h3>
<pre style="background: #fafafa; padding: 20px; border: 1px solid #e0e0e0; margin-bottom: 2em; line-height: 1.9;"># macOS
brew install k6
</pre>
<pre style="background: #fafafa; padding: 20px; border: 1px solid #e0e0e0; margin-bottom: 2em; line-height: 1.9;"># Linux (Debian/Ubuntu)
curl -s https://dl.k6.io/public/release.key | sudo apt-key add -
echo "deb https://dl.k6.io/deb stable main" | sudo tee /etc/apt/sources.list.d/k6.list
sudo apt-get update && sudo apt-get install k6
</pre>
<h3 style="margin-bottom: 45px; line-height: 1.3;">3.2 Define custom metrics (4‑line snippet)</h3>
<pre style="background: #fafafa; padding: 20px; border: 1px solid #e0e0e0; margin-bottom: 2em; line-height: 1.9;">import { Trend, Rate } from 'k6/metrics';
export let latency = new Trend('latency_ms');
export let errors = new Rate('error_rate');
</pre>
<h3 style="margin-bottom: 45px; line-height: 1.3;">3.3 Set test options (5‑line snippet)</h3>
<pre style="background: #fafafa; padding: 20px; border: 1px solid #e0e0e0; margin-bottom: 2em; line-height: 1.9;">export const options = {
stages: [
{ duration: '30s', target: 50 }, // ramp‑up
{ duration: '2m', target: 50 }, // hold
{ duration: '30s', target: 0 }, // ramp‑down
],
thresholds: {
latency_ms: ['p(95)<500'],
error_rate: ['rate<0.01'],
},
};
</pre>
<h3 style="margin-bottom: 45px; line-height: 1.3;">3.4 Build the request payload (4‑line snippet)</h3>
<pre style="background: #fafafa; padding: 20px; border: 1px solid #e0e0e0; margin-bottom: 2em; line-height: 1.9;">const payload = JSON.stringify({ message: 'benchmark' });
const params = {
headers: {
'Content-Type': 'application/json',
Authorization: `Basic ${btoa(`${__ENV.N8N_USER}:${__ENV.N8N_PASS}`)}`,
},
timeout: '60s',
};
</pre>
<h3 style="margin-bottom: 45px; line-height: 1.3;">3.5 Execute the request and record metrics (5‑line snippet)</h3>
<pre style="background: #fafafa; padding: 20px; border: 1px solid #e0e0e0; margin-bottom: 2em; line-height: 1.9;">export default function () {
const res = http.post(`${BASE_URL}/webhook/benchmark`, payload, params);
latency.add(res.timings.duration);
errors.add(!check(res, { 'status is 200': (r) => r.status === 200 }));
sleep(0.2);
}
</pre>
<h3 style="margin-bottom: 45px; line-height: 1.3;">3.6 Run the test</h3>
<pre style="background: #fafafa; padding: 20px; border: 1px solid #e0e0e0; margin-bottom: 2em; line-height: 1.9;">export N8N_URL=http://localhost:5678
export N8N_USER=bench
export N8N_PASS=bench123
k6 run n8n_k6_test.js --out json=results.json
</pre>
<h3 style="margin-bottom: 45px; line-height: 1.3;">3.7 Interpreting k6 output</h3>
<table style="border-collapse: collapse; width: 100%; margin-bottom: 2em;">
<thead>
<tr>
<th style="border: 1px solid #ddd; padding: 13px;">Metric</th>
<th style="border: 1px solid #ddd; padding: 13px;">Meaning</th>
<th style="border: 1px solid #ddd; padding: 13px;">Typical Target</th>
</tr>
</thead>
<tbody>
<tr>
<td style="border: 1px solid #ddd; padding: 13px;">http_req_duration (p95)</td>
<td style="border: 1px solid #ddd; padding: 13px;">95 % percentile of total request time</td>
<td style="border: 1px solid #ddd; padding: 13px;">≤ 500 ms for simple webhook</td>
</tr>
<tr>
<td style="border: 1px solid #ddd; padding: 13px;">latency_ms (custom)</td>
<td style="border: 1px solid #ddd; padding: 13px;">End‑to‑end latency per request</td>
<td style="border: 1px solid #ddd; padding: 13px;">≤ 400 ms</td>
</tr>
<tr>
<td style="border: 1px solid #ddd; padding: 13px;">error_rate</td>
<td style="border: 1px solid #ddd; padding: 13px;">Fraction of failed requests</td>
<td style="border: 1px solid #ddd; padding: 13px;"><1 %</td>
</tr>
<tr>
<td style="border: 1px solid #ddd; padding: 13px;">vus</td>
<td style="border: 1px solid #ddd; padding: 13px;">Peak virtual users</td>
<td style="border: 1px solid #ddd; padding: 13px;">Matches stage target</td>
</tr>
</tbody>
</table>
<p style="margin-bottom: 2em; line-height: 1.9;"><em>EEFA tip:</em> Correlate k6 latency with n8n’s internal <code>request_time_ms</code> log field to verify that network overhead isn’t the bottleneck.</p>
<div style="margin: 50px 0;">
<hr />
</div>
<h2 style="margin-bottom: 45px; line-height: 1.3;">4. Locust Benchmark Script for Stateful Scenarios</h2>
<p style="margin-bottom: 2em; line-height: 1.9;"><strong>Micro‑summary:</strong> Install Locust, create a user class that posts to the webhook and optionally fetches execution details, then run in headless or distributed mode.</p>
<h3 style="margin-bottom: 45px; line-height: 1.3;">4.1 Install Locust</h3>
<pre style="background: #fafafa; padding: 20px; border: 1px solid #e0e0e0; margin-bottom: 2em; line-height: 1.9;">python3 -m venv locust-env
source locust-env/bin/activate
pip install locust
</pre>
<h3 style="margin-bottom: 45px; line-height: 1.3;">4.2 Define authentication header (4‑line snippet)</h3>
<pre style="background: #fafafa; padding: 20px; border: 1px solid #e0e0e0; margin-bottom: 2em; line-height: 1.9;">import base64
user = "bench"
pwd = "bench123"
token = base64.b64encode(f"{user}:{pwd}".encode()).decode()
self.headers = {
"Content-Type": "application/json",
"Authorization": f"Basic {token}",
}
</pre>
<h3 style="margin-bottom: 45px; line-height: 1.3;">4.3 Post to the webhook (5‑line snippet)</h3>
<pre style="background: #fafafa; padding: 20px; border: 1px solid #e0e0e0; margin-bottom: 2em; line-height: 1.9;">@task(5)
def post_webhook(self):
payload = {"message": "locust-bench"}
with self.client.post(
"/webhook/benchmark",
json=payload,
headers=self.headers,
catch_response=True,
) as response:
if response.status_code != 200:
response.failure(f"Bad status: {response.status_code}")
else:
response.success()
</pre>
<h3 style="margin-bottom: 45px; line-height: 1.3;">4.4 Optional execution fetch (4‑line snippet)</h3>
<pre style="background: #fafafa; padding: 20px; border: 1px solid #e0e0e0; margin-bottom: 2em; line-height: 1.9;">@task(1)
def fetch_execution(self):
resp = self.client.get("/executions", headers=self.headers)
if resp.status_code == 200 and resp.json():
exec_id = resp.json()[0]["id"]
self.client.get(f"/executions/{exec_id}", headers=self.headers)
</pre>
<h3 style="margin-bottom: 45px; line-height: 1.3;">4.5 Export a CSV summary on test stop (4‑line snippet)</h3>
<pre style="background: #fafafa; padding: 20px; border: 1px solid #e0e0e0; margin-bottom: 2em; line-height: 1.9;">@events.test_stop.add_listener
def on_test_stop(environment, **kwargs):
with open("locust_summary.csv", "w") as f:
f.write("Name,Requests,Failures,Median,95th\n")
for name, stats in environment.stats.entries.items():
f.write(
f"{name},{stats.num_requests},{stats.num_failures},"
f"{stats.median_response_time},{stats.get_response_time_percentile(95)}\n"
)
</pre>
<h3 style="margin-bottom: 45px; line-height: 1.3;">4.6 Run Locust (headless example)</h3>
<pre style="background: #fafafa; padding: 20px; border: 1px solid #e0e0e0; margin-bottom: 2em; line-height: 1.9;">locust -f locustfile.py --headless -u 100 -r 10 --run-time 5m --csv=run
</pre>
<h3 style="margin-bottom: 45px; line-height: 1.3;">4.7 Distributed mode (optional)</h3>
<pre style="background: #fafafa; padding: 20px; border: 1px solid #e0e0e0; margin-bottom: 2em; line-height: 1.9;"># Master
locust -f locustfile.py --master --expect-workers=3
# Workers (on separate hosts)
locust -f locustfile.py --worker --master-host=MASTER_IP
</pre>
<p style="margin-bottom: 2em; line-height: 1.9;"><em>EEFA note:</em> Locust’s default <code>max_rps</code> is unlimited; in production‑like environments, cap it (<code>--headless -u 200 -r 20</code>) to avoid saturating the network interface before n8n does.</p>
<div style="margin: 50px 0;">
<hr />
</div>
<h2 style="margin-bottom: 45px; line-height: 1.3;">5. Advanced Benchmarking Patterns</h2>
<table style="border-collapse: collapse; width: 100%; margin-bottom: 2em;">
<thead>
<tr>
<th style="border: 1px solid #ddd; padding: 13px;">Pattern</th>
<th style="border: 1px solid #ddd; padding: 13px;">Use‑Case</th>
<th style="border: 1px solid #ddd; padding: 13px;">k6 Implementation</th>
</tr>
</thead>
<tbody>
<tr>
<td style="border: 1px solid #ddd; padding: 13px;">Burst Load</td>
<td style="border: 1px solid #ddd; padding: 13px;">Spike handling (e.g., webhook surge)</td>
<td style="border: 1px solid #ddd; padding: 13px;">Add a rapid ramp stage (<code>target: 200</code> over <code>10s</code>).</td>
</tr>
<tr>
<td style="border: 1px solid #ddd; padding: 13px;">Steady State Throughput</td>
<td style="border: 1px solid #ddd; padding: 13px;">Sustained processing capacity</td>
<td style="border: 1px solid #ddd; padding: 13px;">Hold VUs for 5‑10 min (<code>target: 100</code>).</td>
</tr>
<tr>
<td style="border: 1px solid #ddd; padding: 13px;">Mixed Payloads</td>
<td style="border: 1px solid #ddd; padding: 13px;">Different workflow payload sizes</td>
<td style="border: 1px solid #ddd; padding: 13px;">Parameterize <code>payload</code> with CSV data via <code>options.scenarios</code>.</td>
</tr>
<tr>
<td style="border: 1px solid #ddd; padding: 13px;">End‑to‑End Queue Drain</td>
<td style="border: 1px solid #ddd; padding: 13px;">Measure how fast n8n processes queued jobs after a load burst</td>
<td style="border: 1px solid #ddd; padding: 13px;">Combine k6 with a post‑run script that polls <code>/executions</code> until the queue is empty.</td>
</tr>
<tr>
<td style="border: 1px solid #ddd; padding: 13px;">CI/CD Gate</td>
<td style="border: 1px solid #ddd; padding: 13px;">Fail build if latency > SLA</td>
<td style="border: 1px solid #ddd; padding: 13px;"><code>k6 run … --out json=ci.json && node check-sla.js</code></td>
</tr>
</tbody>
</table>
<table style="border-collapse: collapse; width: 100%; margin-bottom: 2em;">
<thead>
<tr>
<th style="border: 1px solid #ddd; padding: 13px;">Pattern</th>
<th style="border: 1px solid #ddd; padding: 13px;">Locust Implementation</th>
</tr>
</thead>
<tbody>
<tr>
<td style="border: 1px solid #ddd; padding: 13px;">Burst Load</td>
<td style="border: 1px solid #ddd; padding: 13px;">Set a high <code>spawn_rate</code> (<code>-r 100</code>) for a short duration.</td>
</tr>
<tr>
<td style="border: 1px solid #ddd; padding: 13px;">Steady State Throughput</td>
<td style="border: 1px solid #ddd; padding: 13px;"><code>-u 100 -t 10m</code> in headless mode.</td>
</tr>
<tr>
<td style="border: 1px solid #ddd; padding: 13px;">Mixed Payloads</td>
<td style="border: 1px solid #ddd; padding: 13px;">Use weighted <code>@task</code> methods and generate random payloads in the task body.</td>
</tr>
<tr>
<td style="border: 1px solid #ddd; padding: 13px;">End‑to‑End Queue Drain</td>
<td style="border: 1px solid #ddd; padding: 13px;">Add a post‑load <code>@task</code> that repeatedly polls <code>/executions</code> until empty, then record elapsed time.</td>
</tr>
<tr>
<td style="border: 1px solid #ddd; padding: 13px;">CI/CD Gate</td>
<td style="border: 1px solid #ddd; padding: 13px;">Run <code>locust -f locustfile.py --headless -u 50 -r 10 --run-time 2m --csv=ci</code> and parse the CSV for SLA checks.</td>
</tr>
</tbody>
</table>
<p style="margin-bottom: 2em; line-height: 1.9;"><strong>EEFA checklist before CI integration</strong></p>
<ul style="margin-bottom: 1.5em; line-height: 1.9;">
<li>Mirror production DB and Node version in the test environment.</li>
<li>Disable n8n auto‑scaling (if on Kubernetes) to keep node count constant.</li>
<li>Pin k6/Locust versions in <code>package.json</code> or <code>requirements.txt</code>.</li>
<li>Store authentication secrets in CI vault, not in repo.</li>
<li>Add a timeout guard (<code>--max-run-time</code>) to prevent runaway jobs.</li>
</ul>
<div style="margin: 50px 0;">
<hr />
</div>
<h2 style="margin-bottom: 45px; line-height: 1.3;">6. Troubleshooting Common Errors</h2>
<table style="border-collapse: collapse; width: 100%; margin-bottom: 2em;">
<thead>
<tr>
<th style="border: 1px solid #ddd; padding: 13px;">Symptom</th>
<th style="border: 1px solid #ddd; padding: 13px;">Likely Cause</th>
<th style="border: 1px solid #ddd; padding: 13px;">Fix</th>
</tr>
</thead>
<tbody>
<tr>
<td style="border: 1px solid #ddd; padding: 13px;">401 Unauthorized from k6/Locust</td>
<td style="border: 1px solid #ddd; padding: 13px;">Missing or wrong Basic Auth header</td>
<td style="border: 1px solid #ddd; padding: 13px;">Verify <code>N8N_USER</code>/<code>N8N_PASS</code> env vars; base64‑encode correctly.</td>
</tr>
<tr>
<td style="border: 1px solid #ddd; padding: 13px;">socket hang up / ECONNRESET</td>
<td style="border: 1px solid #ddd; padding: 13px;">n8n container hitting open‑file limit (<code>ulimit -n</code>) under load</td>
<td style="border: 1px solid #ddd; padding: 13px;">Increase <code>nofile</code> limit in Docker/K8s (<code>ulimit -n 65535</code>).</td>
</tr>
<tr>
<td style="border: 1px solid #ddd; padding: 13px;">High latency spikes only in CI</td>
<td style="border: 1px solid #ddd; padding: 13px;">Shared CI runner network contention</td>
<td style="border: 1px solid #ddd; padding: 13px;">Isolate load generator on a dedicated VM or use k6 Cloud.</td>
</tr>
<tr>
<td style="border: 1px solid #ddd; padding: 13px;">Metrics show 0 % error but logs contain <code>Execution timed out</code></td>
<td style="border: 1px solid #ddd; padding: 13px;">n8n internal timeout not reflected in HTTP status (still 200)</td>
<td style="border: 1px solid #ddd; padding: 13px;">Enable <code>EXECUTIONS_TIMEOUT=300000</code> to surface timeout as 500 error, or add a custom response check for <code>executionFinished</code> flag.</td>
</tr>
<tr>
<td style="border: 1px solid #ddd; padding: 13px;">Locust UI crashes after many workers</td>
<td style="border: 1px solid #ddd; padding: 13px;">Insufficient RAM on master node</td>
<td style="border: 1px solid #ddd; padding: 13px;">Allocate more memory or limit <code>--expect-workers</code> to realistic count.</td>
</tr>
</tbody>
</table>
<div style="margin: 50px 0;">
<hr />
</div>
<h2 style="margin-bottom: 45px; line-height: 1.3;">7. Exporting Benchmark Results for Stakeholder Reporting</h2>
<h3 style="margin-bottom: 45px; line-height: 1.3;">7.1 Convert k6 JSON to CSV (4‑line snippet)</h3>
<pre style="background: #fafafa; padding: 20px; border: 1px solid #e0e0e0; margin-bottom: 2em; line-height: 1.9;">jq -r '.metrics | to_entries[] | "\(.key),\(.value.values.mean)"' results.json > k6_summary.csv
</pre>
<h3 style="margin-bottom: 45px; line-height: 1.3;">7.2 Locust already writes CSV (see section 4.5)</h3>
<p style="margin-bottom: 2em; line-height: 1.9;">The file <code>locust_summary.csv</code> contains:</p>
<pre style="background: #fafafa; padding: 20px; border: 1px solid #e0e0e0; margin-bottom: 2em; line-height: 1.9;">Name,Requests,Failures,Median,95th
/webhook/benchmark,12000,12,312,498
...
</pre>
<h3 style="margin-bottom: 45px; line-height: 1.3;">7.3 Sample Markdown report (5‑column table split)</h3>
<p style="margin-bottom: 2em; line-height: 1.9;"><strong>k6 Results</strong></p>
<table style="border-collapse: collapse; width: 100%; margin-bottom: 2em;">
<thead>
<tr>
<th style="border: 1px solid #ddd; padding: 13px;">Metric</th>
<th style="border: 1px solid #ddd; padding: 13px;">Peak VUs</th>
<th style="border: 1px solid #ddd; padding: 13px;">Avg Latency (ms)</th>
<th style="border: 1px solid #ddd; padding: 13px;">p95 Latency (ms)</th>
<th style="border: 1px solid #ddd; padding: 13px;">Error Rate</th>
</tr>
</thead>
<tbody>
<tr>
<td style="border: 1px solid #ddd; padding: 13px;">k6</td>
<td style="border: 1px solid #ddd; padding: 13px;">50</td>
<td style="border: 1px solid #ddd; padding: 13px;">312</td>
<td style="border: 1px solid #ddd; padding: 13px;">498</td>
<td style="border: 1px solid #ddd; padding: 13px;">0.2 %</td>
</tr>
</tbody>
</table>
<p style="margin-bottom: 2em; line-height: 1.9;"><strong>Locust Results</strong></p>
<table style="border-collapse: collapse; width: 100%; margin-bottom: 2em;">
<thead>
<tr>
<th style="border: 1px solid #ddd; padding: 13px;">Metric</th>
<th style="border: 1px solid #ddd; padding: 13px;">Peak VUs</th>
<th style="border: 1px solid #ddd; padding: 13px;">Avg Latency (ms)</th>
<th style="border: 1px solid #ddd; padding: 13px;">p95 Latency (ms)</th>
<th style="border: 1px solid #ddd; padding: 13px;">Error Rate</th>
</tr>
</thead>
<tbody>
<tr>
<td style="border: 1px solid #ddd; padding: 13px;">Locust</td>
<td style="border: 1px solid #ddd; padding: 13px;">100</td>
<td style="border: 1px solid #ddd; padding: 13px;">280</td>
<td style="border: 1px solid #ddd; padding: 13px;">470</td>
<td style="border: 1px solid #ddd; padding: 13px;">0.1 %</td>
</tr>
</tbody>
</table>
<p style="margin-bottom: 2em; line-height: 1.9;"><strong>Observations</strong><br />
– Latency stays under the 500 ms SLA up to 100 concurrent webhooks.<br />
– Queue‑drain time after a 2‑minute burst is ~45 s (see *End‑to‑End Queue Drain*).</p>
<p style="margin-bottom: 2em; line-height: 1.9;"><strong>Next Steps</strong><br />
– Increase <code>EXECUTIONS_PROCESS</code> to 2 for higher concurrency.<br />
– Add Redis cache for credential lookups (see the sibling guide on *Docker Performance Tuning*).</p>
<p style="margin-bottom: 2em; line-height: 1.9;">All commands assume a Unix‑like shell. Adjust paths for Windows PowerShell as needed.</p>

Step by Step Guide to solve benchmarking tools
Who this is for: Developers and SREs who need a reproducible way to measure n8n workflow throughput and latency in a CI‑ready, production‑like environment. We cover this in detail in the n8n Performance & Scaling Guide.
Quick Diagnosis
Problem: You need a reproducible way to measure n8n workflow throughput and latency.
Solution: Deploy a lightweight load generator (k6 or Locust), target the n8n REST API (/webhook/... or /executions), run a scripted scenario that mimics real‑world payloads, and capture VU‑seconds, response‑time percentiles, and error rates. Export the results to CSV/JSON for analysis or CI integration. If you encounter any monitoring dashboard setup resolve them before continuing with the setup.
1. Choosing the Right Load Generator for n8n
| Feature |
k6 |
Locust |
When to Prefer |
| Language |
JavaScript (ES6) |
Python |
Use k6 if your team is JS‑centric; Locust if you need complex stateful user flows. |
| CLI‑only vs UI |
CLI only (HTML report optional) |
Web UI + CLI |
Locust’s UI is handy for exploratory testing. |
| Distributed Load |
Built‑in cloud/remote workers |
Master‑worker architecture |
For > 10k VUs, both scale, but k6’s cloud service offers managed scaling. |
| Integration |
InfluxDB, Grafana, CI pipelines |
Prometheus, Grafana, CI pipelines |
Choose based on existing observability stack. |
| License |
Open‑source (MIT) + commercial cloud |
Open‑source (MIT) |
Both free; pick the one that matches your tech stack. |
EEFA note: Both tools generate HTTP traffic only. To benchmark n8n’s internal queue processing, combine the load test with a background worker monitor (see “Measuring Queue Drain Rate” later). If you encounter any logging optimization resolve them before continuing with the setup.
2. Preparing n8n for Benchmarking
Micro‑summary: Set up an isolated n8n instance with a minimal echo workflow and deterministic logging to ensure the benchmark measures HTTP handling, not background processing.
2.1 Spin up a dedicated n8n instance
# docker-compose.yml – n8n service
services:
n8n:
image: n8nio/n8n:latest
environment:
- LOG_LEVEL=debug
- EXECUTIONS_PROCESS=1
- N8N_BASIC_AUTH_ACTIVE=true
- N8N_BASIC_AUTH_USER=bench
- N8N_BASIC_AUTH_PASSWORD=bench123
ports:
- "5678:5678"
2.2 Create a simple “echo” webhook workflow
- Add a Webhook node listening on
/benchmark.
- Connect it directly to a Set node that returns
{{ $json }}.
- Deploy the workflow.
EEFA warning: Do not run benchmarks on a multi‑node cluster without synchronizing EXECUTIONS_PROCESS across pods; otherwise, results will reflect load‑balancer jitter rather than true per‑node capacity. If you encounter any security impact on performance resolve them before continuing with the setup.
3. k6 Benchmark Script for n8n Webhook
Micro‑summary: Install k6, write a short script that posts JSON payloads, and capture custom latency and error metrics.
3.1 Install k6
# macOS
brew install k6
# Linux (Debian/Ubuntu)
curl -s https://dl.k6.io/public/release.key | sudo apt-key add -
echo "deb https://dl.k6.io/deb stable main" | sudo tee /etc/apt/sources.list.d/k6.list
sudo apt-get update && sudo apt-get install k6
3.2 Define custom metrics (4‑line snippet)
import { Trend, Rate } from 'k6/metrics';
export let latency = new Trend('latency_ms');
export let errors = new Rate('error_rate');
3.3 Set test options (5‑line snippet)
export const options = {
stages: [
{ duration: '30s', target: 50 }, // ramp‑up
{ duration: '2m', target: 50 }, // hold
{ duration: '30s', target: 0 }, // ramp‑down
],
thresholds: {
latency_ms: ['p(95)<500'],
error_rate: ['rate<0.01'],
},
};
3.4 Build the request payload (4‑line snippet)
const payload = JSON.stringify({ message: 'benchmark' });
const params = {
headers: {
'Content-Type': 'application/json',
Authorization: `Basic ${btoa(`${__ENV.N8N_USER}:${__ENV.N8N_PASS}`)}`,
},
timeout: '60s',
};
3.5 Execute the request and record metrics (5‑line snippet)
export default function () {
const res = http.post(`${BASE_URL}/webhook/benchmark`, payload, params);
latency.add(res.timings.duration);
errors.add(!check(res, { 'status is 200': (r) => r.status === 200 }));
sleep(0.2);
}
3.6 Run the test
export N8N_URL=http://localhost:5678
export N8N_USER=bench
export N8N_PASS=bench123
k6 run n8n_k6_test.js --out json=results.json
3.7 Interpreting k6 output
| Metric |
Meaning |
Typical Target |
| http_req_duration (p95) |
95 % percentile of total request time |
≤ 500 ms for simple webhook |
| latency_ms (custom) |
End‑to‑end latency per request |
≤ 400 ms |
| error_rate |
Fraction of failed requests |
<1 % |
| vus |
Peak virtual users |
Matches stage target |
EEFA tip: Correlate k6 latency with n8n’s internal request_time_ms log field to verify that network overhead isn’t the bottleneck.
4. Locust Benchmark Script for Stateful Scenarios
Micro‑summary: Install Locust, create a user class that posts to the webhook and optionally fetches execution details, then run in headless or distributed mode.
4.1 Install Locust
python3 -m venv locust-env
source locust-env/bin/activate
pip install locust
4.2 Define authentication header (4‑line snippet)
import base64
user = "bench"
pwd = "bench123"
token = base64.b64encode(f"{user}:{pwd}".encode()).decode()
self.headers = {
"Content-Type": "application/json",
"Authorization": f"Basic {token}",
}
4.3 Post to the webhook (5‑line snippet)
@task(5)
def post_webhook(self):
payload = {"message": "locust-bench"}
with self.client.post(
"/webhook/benchmark",
json=payload,
headers=self.headers,
catch_response=True,
) as response:
if response.status_code != 200:
response.failure(f"Bad status: {response.status_code}")
else:
response.success()
4.4 Optional execution fetch (4‑line snippet)
@task(1)
def fetch_execution(self):
resp = self.client.get("/executions", headers=self.headers)
if resp.status_code == 200 and resp.json():
exec_id = resp.json()[0]["id"]
self.client.get(f"/executions/{exec_id}", headers=self.headers)
4.5 Export a CSV summary on test stop (4‑line snippet)
@events.test_stop.add_listener
def on_test_stop(environment, **kwargs):
with open("locust_summary.csv", "w") as f:
f.write("Name,Requests,Failures,Median,95th\n")
for name, stats in environment.stats.entries.items():
f.write(
f"{name},{stats.num_requests},{stats.num_failures},"
f"{stats.median_response_time},{stats.get_response_time_percentile(95)}\n"
)
4.6 Run Locust (headless example)
locust -f locustfile.py --headless -u 100 -r 10 --run-time 5m --csv=run
4.7 Distributed mode (optional)
# Master
locust -f locustfile.py --master --expect-workers=3
# Workers (on separate hosts)
locust -f locustfile.py --worker --master-host=MASTER_IP
EEFA note: Locust’s default max_rps is unlimited; in production‑like environments, cap it (--headless -u 200 -r 20) to avoid saturating the network interface before n8n does.
5. Advanced Benchmarking Patterns
| Pattern |
Use‑Case |
k6 Implementation |
| Burst Load |
Spike handling (e.g., webhook surge) |
Add a rapid ramp stage (target: 200 over 10s). |
| Steady State Throughput |
Sustained processing capacity |
Hold VUs for 5‑10 min (target: 100). |
| Mixed Payloads |
Different workflow payload sizes |
Parameterize payload with CSV data via options.scenarios. |
| End‑to‑End Queue Drain |
Measure how fast n8n processes queued jobs after a load burst |
Combine k6 with a post‑run script that polls /executions until the queue is empty. |
| CI/CD Gate |
Fail build if latency > SLA |
k6 run … --out json=ci.json && node check-sla.js |
| Pattern |
Locust Implementation |
| Burst Load |
Set a high spawn_rate (-r 100) for a short duration. |
| Steady State Throughput |
-u 100 -t 10m in headless mode. |
| Mixed Payloads |
Use weighted @task methods and generate random payloads in the task body. |
| End‑to‑End Queue Drain |
Add a post‑load @task that repeatedly polls /executions until empty, then record elapsed time. |
| CI/CD Gate |
Run locust -f locustfile.py --headless -u 50 -r 10 --run-time 2m --csv=ci and parse the CSV for SLA checks. |
EEFA checklist before CI integration
- Mirror production DB and Node version in the test environment.
- Disable n8n auto‑scaling (if on Kubernetes) to keep node count constant.
- Pin k6/Locust versions in
package.json or requirements.txt.
- Store authentication secrets in CI vault, not in repo.
- Add a timeout guard (
--max-run-time) to prevent runaway jobs.
6. Troubleshooting Common Errors
| Symptom |
Likely Cause |
Fix |
| 401 Unauthorized from k6/Locust |
Missing or wrong Basic Auth header |
Verify N8N_USER/N8N_PASS env vars; base64‑encode correctly. |
| socket hang up / ECONNRESET |
n8n container hitting open‑file limit (ulimit -n) under load |
Increase nofile limit in Docker/K8s (ulimit -n 65535). |
| High latency spikes only in CI |
Shared CI runner network contention |
Isolate load generator on a dedicated VM or use k6 Cloud. |
Metrics show 0 % error but logs contain Execution timed out |
n8n internal timeout not reflected in HTTP status (still 200) |
Enable EXECUTIONS_TIMEOUT=300000 to surface timeout as 500 error, or add a custom response check for executionFinished flag. |
| Locust UI crashes after many workers |
Insufficient RAM on master node |
Allocate more memory or limit --expect-workers to realistic count. |
7. Exporting Benchmark Results for Stakeholder Reporting
7.1 Convert k6 JSON to CSV (4‑line snippet)
jq -r '.metrics | to_entries[] | "\(.key),\(.value.values.mean)"' results.json > k6_summary.csv
7.2 Locust already writes CSV (see section 4.5)
The file locust_summary.csv contains:
Name,Requests,Failures,Median,95th
/webhook/benchmark,12000,12,312,498
...
7.3 Sample Markdown report (5‑column table split)
k6 Results
| Metric |
Peak VUs |
Avg Latency (ms) |
p95 Latency (ms) |
Error Rate |
| k6 |
50 |
312 |
498 |
0.2 % |
Locust Results
| Metric |
Peak VUs |
Avg Latency (ms) |
p95 Latency (ms) |
Error Rate |
| Locust |
100 |
280 |
470 |
0.1 % |
Observations
– Latency stays under the 500 ms SLA up to 100 concurrent webhooks.
– Queue‑drain time after a 2‑minute burst is ~45 s (see *End‑to‑End Queue Drain*).
Next Steps
– Increase EXECUTIONS_PROCESS to 2 for higher concurrency.
– Add Redis cache for credential lookups (see the sibling guide on *Docker Performance Tuning*).
All commands assume a Unix‑like shell. Adjust paths for Windows PowerShell as needed.