Skip to main content

Apache Bench (ab): A Load Testing Tutorial With Real Output

Apache Bench tutorial: install ab, run a test, read every output line, send POSTs and headers, and avoid the -t 50,000 cap. Tested on ab 2.3 with real output.

Apache Bench (ab): A Load Testing Tutorial With Real Output

Apache Bench (ab) is a command-line tool from the Apache HTTP Server project that sends a set number of HTTP requests to one URL, a set number at a time, and prints requests per second, time per request and a percentile table. A first test is one line: ab -n 1000 -c 10 http://127.0.0.1:8080/.

This tutorial runs ab 2.3 against a small local API and shows what it printed, including four results that trip people up: a POST that failed every time while ab reported zero failures, “failed” requests that all returned HTTP 200, a time limit that stopped after under two seconds, and a concurrency setting that hides how slow a server gets.

Key Takeaways

  • ab sends HTTP/1.0 requests to one URL from one process. It is good for a quick check of a single endpoint, not for user journeys.
  • Failed requests counts connection errors and responses whose length changed. It does not count HTTP 4xx or 5xx: read the separate Non-2xx responses line, which appears only when there are some.
  • POST bodies are sent as text/plain unless you add -T. Our JSON API rejected all 200 POSTs, and ab still exited with code 0.
  • -t caps a run at 50,000 requests unless -n comes after it. Our 5-second run ended after 1.9 seconds.
  • Raising -c past what the server can handle leaves throughput flat and raises time per request, in step with the queue.

What is Apache Bench (ab)?

Apache Bench is the benchmarking program that ships with the Apache HTTP Server, under the Apache License 2.0. It opens a number of connections to one URL, sends requests until it reaches the count you asked for, and prints a summary. It reports itself as “ApacheBench, Version 2.3” and still receives fixes: its source last changed on the 2.4.x branch in June 2026.

It runs as a single process with one event loop and sends HTTP/1.0 requests. The Apache manual’s own Bugs section warns that its heavy string parsing “might indicate a performance problem; i.e., you would measure the ab performance rather than the server’s”, so on a fast endpoint check that ab itself is not the bottleneck before trusting a high requests-per-second figure.

How do you install ab?

ab comes with the Apache HTTP Server, but every major Linux distribution packages it with the server’s other utilities, so you do not need to install or run Apache. On macOS it is already there. Check the version with ab -V.

SystemCommandPackage
macOSnone, it is at /usr/sbin/abbuilt in
Debian, Ubuntusudo apt install apache2-utilsapache2-utils
Fedora, RHEL, Rockysudo dnf install httpd-toolshttpd-tools
Alpineapk add apache2-utilsapache2-utils
Windowsrun the Ubuntu command inside WSLapache2-utils

The Apache project publishes only source code, not Windows binaries, and its Windows page points to third-party builds, so WSL is the simplest route to the same ab that Linux users run. Our tests used the copy built into macOS, which printed This is ApacheBench, Version 2.3 <$Revision: 1923142 $>.

How do you run your first ab test?

Give ab a request count with -n, a concurrency with -c and a full URL that includes a path. -n 1000 -c 10 sends 1,000 requests in total, keeping 10 in flight at any moment: each time one finishes, ab sends the next. Without -n, ab sends a single request.

ab -n 1000 -c 10 http://127.0.0.1:18080/api/products

Our target was a small Node.js API on the same laptop, with a product list, a login, an endpoint that needs a token and a few deliberately slow or failing routes. The product list returned 803 bytes of JSON. ab printed progress lines every 100 requests, then this summary (trimmed: the banner, a second warning about the waiting time, and the 66%, 75%, 80% and 98% rows):

Server Software:
Server Hostname:        127.0.0.1
Server Port:            18080

Document Path:          /api/products
Document Length:        803 bytes

Concurrency Level:      10
Time taken for tests:   0.062 seconds
Complete requests:      1000
Failed requests:        0
Total transferred:      931000 bytes
HTML transferred:       803000 bytes
Requests per second:    16237.98 [#/sec] (mean)
Time per request:       0.616 [ms] (mean)
Time per request:       0.062 [ms] (mean, across all concurrent requests)
Transfer rate:          14763.25 [Kbytes/sec] received

Connection Times (ms)
              min  mean[+/-sd] median   max
Connect:        0    0   0.0      0       0
Processing:     0    1   0.4      0       3
Waiting:        0    1   0.3      0       3
Total:          0    1   0.3      1       3
ERROR: The median and mean for the processing time are more than twice the standard
       deviation apart. These results are NOT reliable.

Percentage of the requests served within a certain time (ms)
  50%      1
  90%      1
  95%      1
  99%      2
 100%      3 (longest request)

Three mistakes stop a first run before it starts. We hit each one on purpose:

What you typedWhat ab printedExit codeFix
http://127.0.0.1:18080 (no path)ab: invalid URL and the usage text22End the URL with a path, at least /
http://localhost:18080/...apr_socket_connect(): Invalid argument (22) after Test aborted after 10 failures22Use 127.0.0.1, or make the server listen on IPv6 too
A port with nothing listeningapr_socket_recv: Connection refused (61)61 on macOS, 111 on LinuxStart the server, check the port

The localhost error is the confusing one. On our Mac, localhost resolves to the IPv6 address ::1 first, and our test server listened only on 127.0.0.1. ab’s source tries the next address when a connection fails, but the run still stopped with Invalid argument. When we started a second server listening on both IPv4 and IPv6, the same localhost command completed all 20 requests.

How do you read ab output?

Start with three lines: Complete requests, Failed requests and, when present, Non-2xx responses. Then read Requests per second and the percentile table. The rest describes the run’s shape: bytes transferred, the timing split per connection, and a warning when the timings are too uneven to summarise with a mean.

What each part means, from the Apache manual and ab’s source:

  • Document Length is the size of the first response ab received, whatever its status code. Later responses of a different size count as failures (see below).
  • Time taken for tests runs from the first connection opening to the last response arriving.
  • Requests per second is completed requests divided by that time.
  • Percentage of the requests served within a certain time is the percentile table, sorted on total time. The 50% row is the median and the 100% row is the slowest request. Percentiles say more than the average about what slow users see.

Time per request appears twice. The first line, the mean, is concurrency × time taken × 1000 / requests: how long one request took from a user’s point of view. The second line, across all concurrent requests, is time taken × 1000 / requests, which is just the inverse of requests per second. Our slow-endpoint run (200 requests, -c 10, 4.256 seconds) printed 212.795 ms and 21.279 ms, which is exactly what the formulas give.

Connection Times split each request: Connect is opening the TCP connection, Waiting is from sending the request to the first byte back, Processing is everything after the connection opened, and Total is the whole request. ab rounds these to whole milliseconds, which is why a fast local test shows rows of zeros and ones.

The ERROR and WARNING lines come from a sanity check in ab’s source. When a timing’s mean and median are more than one standard deviation apart ab prints a WARNING that the results are “probably not that reliable”; more than two prints the ERROR above. On a sub-millisecond endpoint, whole-millisecond rounding sets it off, so take it as a sign that the run was too short or too fast to describe, not that the server misbehaved.

What does ab’s -c concurrency measure?

-c is how many requests ab keeps in flight, not how many users a server can serve. ab waits for each response before reusing that slot, so when the server slows down, ab slows down with it. Past the server’s capacity, more concurrency adds queueing time to every request while requests per second stays flat.

We measured this with an endpoint that has a pool of 4 workers and does 25 ms of work per request, so in theory it can finish 160 requests a second; in practice it topped out near 150. Each row is a separate 400-request run:

-cRequests per secondTime per request (mean)Median99th percentile
136.8027.2 ms27 ms29 ms
272.9127.4 ms28 ms31 ms
4140.5128.5 ms28 ms37 ms
8147.2754.3 ms54 ms56 ms
16148.86107.5 ms106 ms112 ms
32149.12214.6 ms212 ms217 ms

Up to 4 concurrent requests, throughput doubled with each step and response times held at about 27 ms. From 8 onwards throughput stayed near 148 a second, and each doubling of -c doubled the response time, because extra requests only waited in the server’s queue. The capacity was the 4 workers; -c only decided how long the queue was.

The runs at 16 and 32 also reported 290 and 386 failed requests, all Length failures and all HTTP 200: this endpoint’s response includes the current queue length, so its size changed once a queue formed. The section on failed requests below explains why ab counts those.

The same table shows a limit of ab itself. ab never sends faster than responses come back, so a slow server lowers the load ab puts on it. Real visitors keep arriving whether or not the site is slow, which is why tools that send at a fixed rate, such as Vegeta, show much worse response times past capacity. To pick a -c that matches real traffic, work it out from your arrival rate and response time with a concurrent users calculation.

Should you use keep-alive (-k) with ab?

Use -k when your real clients reuse connections, which browsers and most API clients do. Without it, ab opens a new TCP connection for every request, so the test also measures connection setup. With it, ab asks the server to keep each connection open and sends the next request on the same one.

On 5,000 requests to the product endpoint at -c 20, adding -k raised requests per second from 23,667 to 33,831 and shortened the run from 0.211 to 0.148 seconds. ab printed Keep-Alive requests: 5000, so the server honoured every one. If that line shows fewer than your request count, the server closed some connections. Against an HTTPS server, every new connection also needs a TLS handshake, which -k avoids repeating.

How do you send POST requests and headers with ab?

Put the request body in a file and pass it with -p, and set its content type with -T. Add any header with -H, which you can repeat; -C adds a cookie and -A sends basic-auth credentials. ab sends the same body and headers on every request, so it cannot log in once and reuse a token it receives.

printf '{"user":"demo","password":"demo123"}' > login.json
ab -n 200 -c 5 -p login.json -T application/json http://127.0.0.1:18080/api/login
ab -n 200 -c 5 -H "Authorization: Bearer $TOKEN" http://127.0.0.1:18080/api/me

Both runs completed 200 requests with no failures. To test an endpoint behind a login, fetch a token first with curl and pass it in -H, as we did with $TOKEN.

Leave out -T and every POST can fail without ab reporting a failure. ab then sends Content-type: text/plain, which we confirmed with -v 4, a verbosity level that prints each request’s headers. Our API answered every one of the 200 requests with HTTP 415 Unsupported Media Type. ab printed Complete requests: 200, Failed requests: 0 and Non-2xx responses: 200, and exited with code 0. The same happened without the Authorization header: all 200 requests got HTTP 401, and ab still reported zero failed requests.

Why does ab report failed requests when the responses were fine?

ab counts a response as failed when its length differs from the first response’s, because it was written for static files that always return the same bytes. A dynamic page or API whose body length varies produces “failures” that are all HTTP 200. Add -l to stop treating a length change as an error.

Against an endpoint that returns a random number, 47 of 500 responses were failures, all listed as (Connect: 0, Receive: 0, Length: 47, Exceptions: 0). With -l, the same run printed Document Length: Variable and Failed requests: 0.

The rule cuts both ways. An endpoint that returned HTTP 500 to 92 of 1,000 requests showed 92 Length failures and Non-2xx responses: 92, but only because our error body happened to be longer than the success body. An error page of the same length as the success page would show as zero failed requests. Read Non-2xx responses for status codes and Failed requests for connection and length problems.

How do you run ab for a fixed amount of time?

Use -t with a number of seconds, and add -n after it with a request count high enough not to be reached first. -t on its own sets the request count to 50,000, which ab’s source uses to size its results table, and ab stops at whichever limit it reaches first.

Our results, against the fast product endpoint at -c 20:

CommandTime takenComplete requests
ab -t 5 -c 20 <url>1.903 s50,000
ab -n 2000000 -t 3 -c 20 <url>1.987 s50,000
ab -t 3 -n 2000000 -c 20 <url>3.000 s73,184

Order matters: -t resets the count to 50,000 when ab reads it, so an -n placed before -t is overwritten. The manual says -t “implies a -n 50000 internally” but not that a later -n lifts the cap. On a slower endpoint you may never reach 50,000 and never notice.

How do you save ab results to a file?

ab has two file outputs. -e file.csv writes the percentile table at every whole percent from 0 to 100, in milliseconds, as 101 rows under a header. -g file.tsv writes one row per request with its start time and its connect, processing, total and waiting times, for a spreadsheet or gnuplot.

ab -n 2000 -c 10 -e percentiles.csv -g requests.tsv http://127.0.0.1:18080/api/report

Our 2,000-request run wrote a 102-line CSV and a 2,001-line TSV, whose header is starttime seconds ctime dtime ttime wait. Save the plain-text summary as well (> run.txt); neither file holds the request counts or the failure lines.

Can you use ab in a CI pipeline?

Yes, but its exit code will not fail the build for you. ab exits with 0 whenever the run completes, including when every response was an HTTP 401, 415 or 500. It exits non-zero only when the run cannot start or is aborted, such as a bad URL (22), a refused connection (61 on macOS, 111 on Linux) or, unless you pass -r, a receive error partway through. Check the summary text yourself.

#!/bin/sh
# Fail the build when ab saw failed or non-2xx responses.
ab -q -n 1000 -c 10 "$1" > ab.txt || exit 1
cat ab.txt
if ! grep -q '^Failed requests: *0$' ab.txt || grep -q '^Non-2xx responses:' ab.txt; then
  echo "ab: failed or non-2xx responses" >&2
  exit 1
fi

We ran this script against two endpoints: it exited 0 for the product endpoint and 1 for the one that returns occasional 500s. -q hides the progress lines. Add -l if the endpoint’s response length varies, and a check on Requests per second or the 95% row if you want a performance threshold too.

When is ab not enough?

ab answers one question well: how fast does this one URL respond to this many parallel requests from this machine. It cannot follow a journey across pages, ramp the load up over time, send traffic from other countries, or show results while a test runs.

For whole journeys you need a scripting tool; our k6 vs JMeter vs Gatling vs Locust comparison covers four of them. LoadFocus runs load tests from 25+ cloud regions. You can build a test in the browser or upload a JMeter or k6 script for logins and multi-step journeys, and there are no load generators to set up. The Apache Bench alternative page compares the two side by side, including where ab remains the better choice, such as quick checks during development and networks with no internet access. For tests that go beyond one machine, see cloud load testing.

Frequently Asked Questions

What does ab stand for in Apache Bench?

ab is short for ApacheBench, the HTTP benchmarking program that ships with the Apache HTTP Server. You run it as ab from the command line, and ab -V prints its version. The current version is 2.3, and its source still receives fixes: it last changed in June 2026.

Does Apache Bench support HTTP/2?

No. ab sends HTTP/1.0 requests, which we confirmed by printing the request headers with -v 4, and it has no option for HTTP/2 or HTTP/3. Its only connection reuse is HTTP/1.0 keep-alive, with -k. To test HTTP/2 behaviour, use a tool that supports it, such as Vegeta, which by default sends HTTP/2 to HTTPS servers that support it (-h2c for plain HTTP).

Why does ab show failed requests when my server returned 200?

ab marks a response as failed when its length differs from the first response’s, because it expects every response to be identical. Pages and APIs that return different content each time trigger this with every response still at HTTP 200. Run ab with -l to stop counting length changes as failures, and read the Non-2xx responses line for real HTTP errors.

How do I send a POST request with Apache Bench?

Save the body in a file, pass it with -p and set its content type with -T, for example ab -n 100 -c 5 -p body.json -T application/json http://127.0.0.1:8080/api/login. Without -T, ab sends Content-type: text/plain, and a JSON API may reject every request while ab still reports zero failed requests.

Why does ab -t stop before the time limit?

-t sets ab’s request limit to 50,000, and ab stops at whichever limit it reaches first, so a fast endpoint can finish 50,000 requests well before the time runs out. Against a fast local endpoint, a 5-second run stopped after 1.9 seconds. Add -n with a larger number after -t, as in ab -t 60 -n 2000000 -c 20 <url>. ab reserves 32 bytes of memory per request before it starts, so 2,000,000 takes about 64 MB; size -n near what the run can reach rather than at an extreme.

Sources

  • Options, output fields and known bugs: httpd.apache.org/docs/2.4/programs/ab.html, “ab - Apache HTTP server benchmarking tool”, October 2026.
  • Timing rows, reliability warnings, the 50,000-request limit for -t and the single event loop: support/ab.c on the httpd 2.4.x branch (last changed 4 June 2026), October 2026.
  • Apache HTTP Server 2.4.69, released 1 October 2026: httpd.apache.org, October 2026.
  • Windows binaries: httpd.apache.org/docs/2.4/platform/windows.html, “Downloading httpd for Windows”, October 2026.
  • Packages that ship ab: packages.ubuntu.com (apache2-utils, noble), packages.debian.org (apache2-utils, trixie), src.fedoraproject.org (httpd.spec, %files tools), pkgs.alpinelinux.org (apache2-utils), October 2026.
  • Test runs: ab 2.3 (Revision 1923142, built into macOS) against a local Node.js 23.11.1 test API on 127.0.0.1, 29 October 2026. Timings are from one laptop running other work and show patterns, not benchmarks.

Bogdan

Founder at LoadFocus

Bogdan builds and runs the tools this blog is about. He writes from what the products actually do in production, including the parts that break.

Weekly tips right in your inbox.

Don’t miss out on the latest testing tips and tricks, tools and news related to software testing.

Already +20,000 members in the list.

The Load Testing Platform with AI That Explains Your Results

Load test websites and APIs from 25 cloud regions, monitor page speed and uptime, and get AI analysis that explains your results in plain English.

Cloud load testing, website speed and API monitoring in one place.

Hi there! Can I share a cool thing I'm working on with you?

A software testing newsletter that sparks joy.

Our goal is to create helpful content for software developers and testers.

If you're not into it, you can unsubscribe instantly.