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 sudo user. This is where ab runs.
  • A target web server you are allowed to load test, reachable at your_server_ip or your_domain, for example a CubePath VPS running Nginx or Apache.
  • Ideally, client and target on different machines in the same region. Running ab on the server itself measures a server that is also busy generating the load.

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/

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:

FieldWhat it means
Complete requests / Failed requestsEvery request must complete. Any failure means the numbers above it are not trustworthy.
Non-2xx responsesOnly shown when some responses were errors or redirects (for example 404, 301, 502, 503). A fast benchmark of error pages is meaningless.
Requests per secondThroughput: 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 TimesConnect is TCP (and TLS) setup, Waiting is time to first byte, Processing is waiting plus receiving the body.
Percentiles50% 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 requests broken down as Length: 37 means responses had different sizes. That is normal for dynamic pages with timestamps or CSRF tokens. Rerun with -l to 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.

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, -c and -k identical 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 with ulimit -n 65535 and 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_connections in Nginx, MaxRequestWorkers in 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 60 if slow responses are expected.
  • Very high Requests per second with many Non-2xx responses: you are benchmarking error pages or redirects. Fix the URL (for example use https:// 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.