Benchmarking Nginx with wrk turns tuning work into numbers that can be compared across changes, making regressions obvious and improvements defensible. A repeatable baseline is especially useful when adjusting worker counts, buffers, caching, TLS settings, or upstream timeouts.
wrk is a multi-threaded load generator for HTTP/1.1 that keeps many connections open and issues requests as fast as possible, reporting throughput plus average and percentile latency when --latency is enabled. Results are sensitive to request shape (path, headers, body size), connection reuse (keep-alive), and network conditions, so a single well-defined URL and a fixed option set matter more than absolute numbers.
Synthetic load can overwhelm a server and also mislead when the load generator becomes CPU- or network-bound, so runs belong in controlled test windows with server-side metrics visible. Prefer a dedicated client host close to the server, keep the target endpoint stable (avoid redirects), and treat each run as a comparison between configurations rather than a promise of production capacity.
Steps to benchmark Nginx with wrk:
- Confirm the benchmark URL returns 200 with no redirects.
$ curl --head http://127.0.0.1/ HTTP/1.1 200 OK Server: nginx Date: Tue, 30 Dec 2025 00:33:14 GMT Content-Type: text/html Content-Length: 20 Connection: keep-alive Keep-Alive: timeout=15 Vary: Accept-Encoding Last-Modified: Mon, 29 Dec 2025 22:23:50 GMT X-Cache-Status: STALE
A 301/308 redirect adds an extra request per hit, so benchmark the final URL (often by adding a trailing /).
- Record the Nginx version plus the exact configuration under test before capturing a baseline.
$ nginx -v 2>&1 nginx version: nginx/1.24.0
Capturing effective config with nginx -T improves repeatability, but the output may include sensitive paths and upstream details.
- Run a short warm-up from the load generator to stabilize caches before measuring.
$ wrk --threads 2 --connections 50 --duration 10s --timeout 10s http://127.0.0.1/ Running 10s test @ http://127.0.0.1/ 2 threads and 50 connections Thread Stats Avg Stdev Max +/- Stdev Latency 250.52us 802.04us 24.75ms 97.37% Req/Sec 153.69k 18.08k 176.47k 83.50% 3057258 requests in 10.01s, 798.80MB read Requests/sec: 305486.48 Transfer/sec: 79.82MBIf the client CPU is saturated, results reflect the load generator rather than Nginx.
- Run a baseline benchmark with --latency enabled while saving the output for comparison.
$ wrk --threads 2 --connections 50 --duration 30s --timeout 10s --latency http://127.0.0.1/ | tee wrk-baseline.txt Running 30s test @ http://127.0.0.1/ 2 threads and 50 connections Thread Stats Avg Stdev Max +/- Stdev Latency 323.52us 1.92ms 59.35ms 98.78% Req/Sec 158.70k 16.00k 176.03k 88.00% Latency Distribution 50% 106.00us 75% 188.00us 90% 394.00us 99% 3.31ms 9477534 requests in 30.06s, 2.42GB read Socket errors: connect 0, read 0, write 0, timeout 18 Requests/sec: 315322.04 Transfer/sec: 82.39MBDo not run load tests against production endpoints without capacity planning, approval, and a rollback plan.
- Query stub_status during the run to spot saturation signals like rising active connections or stalled requests.
$ curl -s http://127.0.0.1/nginx_status Active connections: 1 server accepts handled requests 23485 23485 22959263 Reading: 0 Writing: 1 Waiting: 0
Auto-refresh with watch -n 1 curl -s http://127.0.0.1/nginx_status when interactive monitoring is needed.
- Check /var/log/nginx/error.log immediately after the run for timeouts, upstream errors, or worker crashes.
$ sudo tail -n 50 /var/log/nginx/error.log 2025/12/30 00:27:10 [error] 5922#5922: *10932 client intended to send too large body: 11534336 bytes, client: 127.0.0.1, server: _, request: "POST / HTTP/1.1", host: "127.0.0.1" 2025/12/30 00:29:29 [error] 6074#6074: *10940 access forbidden by rule, client: 127.0.0.2, server: _, request: "GET /admin/ HTTP/1.1", host: "127.0.0.1" ##### snipped #####
Latency improvements are not meaningful if the run introduces 5xx errors or upstream failures.
- Repeat the benchmark after each tuning change while keeping the URL and wrk options identical.
$ wrk --threads 2 --connections 50 --duration 30s --timeout 10s --latency http://127.0.0.1/ | tee wrk-after-change.txt Running 30s test @ http://127.0.0.1/ 2 threads and 50 connections Thread Stats Avg Stdev Max +/- Stdev Latency 294.37us 1.42ms 47.83ms 98.42% Req/Sec 155.06k 17.51k 175.39k 87.50% Latency Distribution 50% 111.00us 75% 185.00us 90% 365.00us 99% 3.66ms 9259286 requests in 30.03s, 2.36GB read Requests/sec: 308322.41 Transfer/sec: 80.56MBOnly one variable should change per comparison run, otherwise the cause of a difference is ambiguous.
- Increase --connections in small steps to find the point where latency spikes or socket timeouts appear.
$ wrk --threads 2 --connections 200 --duration 30s --timeout 10s --latency http://127.0.0.1/ | tee wrk-c200.txt Running 30s test @ http://127.0.0.1/ 2 threads and 200 connections Thread Stats Avg Stdev Max +/- Stdev Latency 1.31ms 7.77ms 168.98ms 98.70% Req/Sec 163.66k 20.34k 188.79k 87.42% Latency Distribution 50% 352.00us 75% 745.00us 90% 1.42ms 99% 16.21ms 9727213 requests in 30.04s, 2.48GB read Requests/sec: 323783.38 Transfer/sec: 84.60MBDriving the server into sustained timeouts can trigger autoscaling, upstream circuit breakers, or cascading failures in shared environments.
- Compare Requests/sec and percentile latency across saved results to confirm the desired direction of change.
$ grep -E "Requests/sec:|50%|90%|99%" -n wrk-*.txt wrk-after-change.txt:7: 50% 111.00us wrk-after-change.txt:9: 90% 365.00us wrk-after-change.txt:10: 99% 3.66ms wrk-after-change.txt:12:Requests/sec: 308322.41 ##### snipped #####
Mohd Shakir Zakaria is a cloud architect with deep roots in software development and open-source advocacy. Certified in AWS, Red Hat, VMware, ITIL, and Linux, he specializes in designing and managing robust cloud and on-premises infrastructures.
