--- title: "How to Benchmark a Web Server with Apache Bench (ab)" description: "Install Apache Bench on Ubuntu 24.04, run load tests against any web server, read requests per second and latency percentiles, and compare results reliably." category: "Performance Optimization" language: en source: https://cubepath.com/docs/performance-optimization/web-server-benchmarking-with-apache-bench-ab updated: 2026-09-25 --- # How to Benchmark a Web Server with Apache Bench (ab) 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. > Warning: Only 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: ```bash sudo apt update sudo apt install apache2-utils ``` Check that it is available: ```bash ab -V ``` ```output 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: ```bash ab -n 1000 -c 10 http://your_server_ip/ ``` > Note: The URL must include a path. `ab http://your_server_ip` fails with `ab: invalid URL`; add the trailing slash. `ab` prints progress every 10% of the requests, then a report like this: ```output 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 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: ```bash 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: ```output 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. ```bash 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: ```output 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`: ```bash 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: ```bash nano payload.json ``` ```json {"name": "benchmark", "email": "test@example.com"} ``` Send it as a POST with `-p` (body file) and `-T` (content type): ```bash 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`: ```bash 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. > Note: If 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: ```bash 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: ```bash 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.