Apache Bench (ab) is a small command line tool that sends a fixed number of HTTP requests to a URL with a chosen concurrency and reports throughput and response times. Despite its name it works against any HTTP server: Nginx, Apache, Caddy, a Node.js app or an API behind a load balancer. In this tutorial you will install ab on Ubuntu 24.04, run a first benchmark, learn to read its output, and use it to compare configurations in a way that gives repeatable numbers.
Prerequisites
To follow this tutorial, you will need:
- A client machine running Ubuntu 24.04 LTS with a non-root
sudouser. This is whereabruns. - A target web server you are allowed to load test, reachable at
your_server_iporyour_domain, for example a CubePath VPS running Nginx or Apache. - Ideally, client and target on different machines in the same region. Running
abon the server itself measures a server that is also busy generating the load.
WarningOnly benchmark servers you own or have written permission to test. A load test against someone else's site is indistinguishable from a denial-of-service attack.
Step 1 - Installing Apache Bench
ab is shipped in the apache2-utils package, which does not install the Apache web server itself:
sudo apt update
sudo apt install apache2-utils
Check that it is available:
ab -V
This is ApacheBench, Version 2.3 <$Revision: 1913912 $>
Copyright 1996 Adam Twiss, Zeus Technology Ltd, http://www.zeustech.net/
Licensed to The Apache Software Foundation, http://www.apache.org/
Step 2 - Running your first benchmark
The two options you will always use are -n (total number of requests) and -c (how many requests run at the same time). Send 1,000 requests, 10 at a time:
ab -n 1000 -c 10 http://your_server_ip/
NoteThe URL must include a path.
ab http://your_server_ipfails withab: invalid URL; add the trailing slash.
ab prints progress every 10% of the requests, then a report like this:
Server Software: nginx/1.24.0
Server Hostname: 203.0.113.10
Server Port: 80
Document Path: /
Document Length: 615 bytes
Concurrency Level: 10
Time taken for tests: 0.412 seconds
Complete requests: 1000
Failed requests: 0
Total transferred: 853000 bytes
HTML transferred: 615000 bytes
Requests per second: 2427.18 [#/sec] (mean)
Time per request: 4.120 [ms] (mean)
Time per request: 0.412 [ms] (mean, across all concurrent requests)
Transfer rate: 2021.87 [Kbytes/sec] received
Connection Times (ms)
min mean[+/-sd] median max
Connect: 1 2 0.4 2 4
Processing: 1 2 0.6 2 7
Waiting: 1 2 0.6 2 7
Total: 2 4 0.8 4 9
Percentage of the requests served within a certain time (ms)
50% 4
66% 4
75% 4
80% 4
90% 5
95% 5
98% 6
99% 7
100% 9 (longest request)
Step 3 - Reading the results
The report has a lot of lines, but only a few matter for most decisions:
| Field | What it means |
|---|---|
Complete requests / Failed requests | Every request must complete. Any failure means the numbers above it are not trustworthy. |
Non-2xx responses | Only shown when some responses were errors or redirects (for example 404, 301, 502, 503). A fast benchmark of error pages is meaningless. |
Requests per second | Throughput: total requests divided by total time. The headline number when comparing configurations. |
Time per request (mean) | Average latency seen by one client, including waiting in the concurrency queue. |
Connection Times | Connect is TCP (and TLS) setup, Waiting is time to first byte, Processing is waiting plus receiving the body. |
| Percentiles | 50% is the median, 95% and 99% show the slow tail users actually notice. |
Two patterns to look for:
- Latency tail. If the median is 4 ms but
99%is 400 ms, something (a slow query, a full worker pool, garbage collection) blocks a small fraction of requests. The mean hides this. - Length failures.
Failed requestsbroken down asLength: 37means responses had different sizes. That is normal for dynamic pages with timestamps or CSRF tokens. Rerun with-lto accept variable lengths so only real errors are counted.
Step 4 - Enabling keep-alive
By default ab opens a new TCP connection for every request, which is not how browsers behave and makes the connection setup dominate the result, especially with HTTPS. The -k option reuses connections with HTTP keep-alive:
ab -k -n 5000 -c 50 http://your_server_ip/
The report now includes the number of reused connections, and throughput is usually much higher:
Keep-Alive requests: 5000
Requests per second: 9823.41 [#/sec] (mean)
Use -k for realistic tests of a website. Test without it when you specifically want to measure connection or TLS handshake cost.
Step 5 - Finding the saturation point
A single run tells you little. Increase concurrency step by step and watch where throughput stops growing and latency starts rising: that is the capacity of the server for that page.
for c in 10 50 100 200 400; do
printf 'c=%-4s' "$c"
ab -k -q -n 10000 -c "$c" http://your_server_ip/ | grep -E 'Requests per second|Failed requests| 99%' | tr -s ' ' | paste -sd ' '
done
-q suppresses the progress lines. Output looks similar to:
c=10 Failed requests: 0 Requests per second: 7910.33 [#/sec] (mean) 99% 3
c=50 Failed requests: 0 Requests per second: 11820.54 [#/sec] (mean) 99% 9
c=100 Failed requests: 0 Requests per second: 12104.12 [#/sec] (mean) 99% 18
c=200 Failed requests: 0 Requests per second: 11987.65 [#/sec] (mean) 99% 41
c=400 Failed requests: 0 Requests per second: 11650.02 [#/sec] (mean) 99% 97
Here throughput stops growing around 100 concurrent requests while the 99th percentile keeps doubling. Beyond that point you are only adding queueing delay. While the test runs, watch the target server with htop or vmstat 1 in another terminal to see whether CPU, memory, or a backend (PHP-FPM, database) is the limit.
For a longer, sustained test, use -t to run for a number of seconds instead of a fixed request count. When -t is set, ab stops at 50,000 requests unless you also raise -n:
ab -k -t 60 -n 10000000 -c 100 http://your_server_ip/
Step 6 - Testing POST requests, headers and authentication
APIs often need a request body, headers or credentials. ab covers the common cases.
Create a JSON body file:
nano payload.json
{"name": "benchmark", "email": "[email protected]"}
Send it as a POST with -p (body file) and -T (content type):
ab -n 1000 -c 20 -p payload.json -T 'application/json' http://your_server_ip/api/items
Add request headers with -H (repeatable), cookies with -C, and HTTP basic authentication with -A:
ab -n 1000 -c 20 -H 'Authorization: Bearer your_api_token' http://your_server_ip/api/items
ab -n 1000 -c 20 -C 'session_id=your_session_id' http://your_server_ip/account/
ab -n 1000 -c 20 -A your_user:your_password http://your_server_ip/private/
HTTPS URLs work the same way, for example ab -k -n 1000 -c 20 https://your_domain/. Keep in mind that ab speaks HTTP/1.0 (with keep-alive as an extension) and does not support HTTP/2.
NoteIf the endpoint writes to a database, point the test at a staging environment. A thousand POST requests create a thousand records.
Step 7 - Saving results and comparing runs
Numbers are only useful when compared under the same conditions. Save each run so you can compare before and after a change.
-e writes the full percentile table (0-100%) to a CSV file, and -g writes per-request timings in a TSV format that spreadsheets and gnuplot can read:
ab -k -n 10000 -c 100 -e before.csv -g before.tsv http://your_server_ip/ > before.txt
Make your change (for example enable compression or tune worker counts), reload the server, and repeat with new filenames:
ab -k -n 10000 -c 100 -e after.csv -g after.tsv http://your_server_ip/ > after.txt
grep -E 'Requests per second|Failed| 95%| 99%' before.txt after.txt
To keep comparisons honest:
- Run each test 3 times and use the median, and discard a first warm-up run that fills caches.
- Keep the client, network path, URL,
-n,-cand-kidentical between runs. - Test the pages that matter (a dynamic page, an API endpoint) and not only the static home page, which is almost always fast.
Troubleshooting
socket: Too many open files (24): the client ran out of file descriptors at high concurrency. Raise the limit for the current shell withulimit -n 65535and run the test again.apr_socket_recv: Connection reset by peer (104): the server or a firewall dropped connections. Check the server's error log and connection limits (worker_connectionsin Nginx,MaxRequestWorkersin Apache) and any rate limiting in front of it.apr_pollset_poll: The timeout specified has expired (70007): responses took longer than the default 30-second timeout. The server is overloaded at this concurrency. Lower-c, or raise the timeout with-s 60if slow responses are expected.- Very high
Requests per secondwith manyNon-2xx responses: you are benchmarking error pages or redirects. Fix the URL (for example usehttps://or the final path after the redirect).
Conclusion
You installed Apache Bench, ran benchmarks with and without keep-alive, found the saturation point of a page, tested authenticated and POST endpoints, and saved results to compare changes. ab is ideal for quick, single-URL checks. For multi-URL scenarios, scripted requests, or generating more load from a single client, continue with wrk and siege, and use tools such as perf or strace on the server to find out why it slows down at the saturation point.
