You shipped an endpoint. It returns the right JSON in Postman. But you have no idea what happens when 200 clients hit it at once. Does it hold 500 requests per second, or does latency fall apart at 50? ApacheBench answers that question in one command.
ApacheBench, invoked as ab, is a command-line tool that fires a fixed number of HTTP requests at a single URL and reports throughput and latency. It ships with Apache HTTP Server and has been around for decades. It’s small, it’s fast to run, and it gives you a quick read on how much load one endpoint can take.
This guide covers installing ab, running a basic test, testing POST endpoints, reading the output, and knowing where ab stops being the right tool. Every command here maps to a flag documented in the official Apache ab reference.
What ApacheBench is and where it comes from
ab is a benchmarking client bundled with Apache HTTP Server. The name is short for ApacheBench. It opens connections to one URL, sends requests as fast as your concurrency setting allows, times each response, and prints a summary.

It measures one thing well: how many requests per second a single endpoint can serve, and how response time is distributed under load. That’s throughput and latency for one URL.
What it does not do matters as much. ab doesn’t check whether the response body is correct. It doesn’t validate a schema or assert on a field. It doesn’t walk a multi-step flow like login, then fetch, then update. It hits one URL and counts. Keep that scope in mind and ab stays useful. Expect more and you’ll be disappointed.
If you want a broader view of the discipline ab fits into, see what API load testing is and why API response time matters.
Installing ab
ab comes packaged with Apache utilities. The package name differs by platform.
On Debian and Ubuntu, install the apache2-utils package:
sudo apt-get update
sudo apt-get install -y apache2-utils
On CentOS, RHEL, and Fedora, the package is httpd-tools:
# CentOS 7
sudo yum install -y httpd-tools
# Fedora and CentOS 8+
sudo dnf install -y httpd-tools
On macOS, ab is already present because the system ships with Apache. Check it:
ab -V
You should see a version line like Version 2.3. If the command is missing on macOS, install it through Homebrew’s Apache formula. Once ab -V prints a version, you’re ready.
Running a basic load test
The two flags you’ll use every time are -n and -c.
-n requestssets the total number of requests to perform. The default is a single request, which tells you nothing, so always set this.-c concurrencysets how many requests run at a time. The default is one, meaning fully serial.
Here’s 1000 requests, 50 at a time, against a JSON endpoint:
ab -n 1000 -c 50 https://api.example.com/v1/users
Note the trailing path. ab needs a full URL with a path. If you point it at a bare host with no path, it errors out. For the root, use a trailing slash: https://api.example.com/.
Real clients reuse TCP connections instead of opening a fresh one per request. Add -k to enable HTTP KeepAlive so ab reuses connections within a session:
ab -n 1000 -c 50 -k https://api.example.com/v1/users
The -k run is usually closer to how a browser or a well-behaved API client behaves, since it avoids paying the connection setup cost on every request. Compare the two numbers to see how much of your latency is connection overhead.
You can also cap the run by time instead of count. -t sets a maximum number of seconds and implies -n 50000 internally, so the test stops at whichever limit you hit first:
ab -t 30 -c 50 -k https://api.example.com/v1/users
That runs for up to 30 seconds at concurrency 50. Handy when you want a fixed-duration read rather than a fixed count.
Testing a POST endpoint
Most API work isn’t GET. To test a POST, put the request body in a file and pass it with -p. You also have to set the content type with -T, or the server will reject the body.
Create the payload:
cat > payload.json <<'EOF'
{"name": "Ada Lovelace", "email": "ada@example.com"}
EOF
Then send it:
ab -n 500 -c 25 -k \
-p payload.json \
-T application/json \
https://api.example.com/v1/users
The -p flag names the file containing the POST body. The -T flag sets the Content-Type header. The default content type is text/plain, which is almost never what a JSON API wants, so set -T application/json explicitly.
If your endpoint needs auth or other headers, add them with -H. Repeat the flag for each header:
ab -n 500 -c 25 -k \
-p payload.json \
-T application/json \
-H "Authorization: Bearer YOUR_TOKEN" \
https://api.example.com/v1/users
For a refresher on building JSON request bodies by hand, see how to POST JSON data with curl. The same body works as an ab payload file.
Reading the output
A run prints a block of numbers. Here’s a trimmed example:
Concurrency Level: 50
Time taken for tests: 4.212 seconds
Complete requests: 1000
Failed requests: 0
Non-2xx responses: 0
Requests per second: 237.42 [#/sec] (mean)
Time per request: 210.598 [ms] (mean)
Time per request: 4.212 [ms] (mean, across all concurrent requests)
Transfer rate: 142.31 [Kbytes/sec] received
Connection Times (ms)
min mean[+/-sd] median max
Connect: 5 18 9.4 16 64
Processing: 22 189 41.2 182 310
Waiting: 21 177 39.8 171 295
Total: 31 207 42.7 201 338
Percentage of the requests served within a certain time (ms)
50% 201
66% 221
75% 236
90% 268
95% 291
99% 324
100% 338 (longest request)
Read these fields first:
- Requests per second is your throughput headline. It’s the request count divided by total time. Higher is better.
- Failed requests and Non-2xx responses tell you whether the server actually handled the load or started erroring. A high requests-per-second number means nothing if half the responses are 500s. Always check these two before you trust the throughput figure.
- Time per request appears twice. The first line is the mean time a single user waits (concurrency times total time divided by requests). The second line, labeled “across all concurrent requests,” is the average gap between completed requests. The first number is the one users feel.
The Percentage of the requests served within a certain time table is the most useful part. It’s a percentile breakdown. The 50% row is your median. The 95% and 99% rows show the slow tail. In the example, half of requests finished by 201ms, but 1% took 324ms or longer. Tail latency is where real users get hurt, so watch the 95th and 99th percentiles more than the mean.
Want the raw per-request numbers for a chart? Add -e results.csv to write a CSV of percentile-to-time pairs, or -g results.tsv for a gnuplot-friendly file.
Where ab stops being the right tool
ab is deliberately narrow. Its limits are worth stating plainly so you don’t misuse it.
One URL per run. ab hammers a single endpoint. It can’t script a sequence like authenticate, create a resource, then read it back. Real user journeys touch many endpoints in order, and ab has no concept of that.
No correctness checks. ab counts status codes and byte lengths. It doesn’t parse your JSON or assert that a field equals an expected value. A response can be structurally wrong and still count as a success. Load numbers are not a substitute for a passing test suite.
Dated HTTP handling. The official docs state ab does not implement HTTP/1.x fully and only accepts some expected forms of responses. It has no HTTP/2 support. For servers that behave differently under HTTP/2, ab won’t reflect that.
Single-machine load. ab runs from one box and one process. Past a certain concurrency, you measure your own client’s limits instead of the server’s. The docs even warn that ab’s own parsing can show up as a bottleneck in profiles.
None of this makes ab bad. It makes it a specific tool: a fast throughput probe for one endpoint. When you need scripted flows, distributed load, or HTTP/2, reach for something built for that. JMeter handles multi-step scenarios and distributed runs, and there’s a broader roundup of load testing tools if you’re comparing options.
Where functional testing fits
Throughput is one axis. Correctness is another, and ab doesn’t touch it. Before you load test an endpoint, you want to know it returns the right data with the right shape under normal conditions. That’s functional and contract testing, and it’s a different job.
This is where a full API platform earns its place next to ab. Apidog lets you build test scenarios with visual assertions, chain requests into multi-step flows, and validate responses against your schema, the things ab can’t do by design. You save those scenarios once and run them anywhere.
For continuous integration, the Apidog CLI runs your saved scenarios headlessly. Install it with Node:
npm install -g apidog-cli
Then run a saved scenario or suite against a chosen environment, emitting reports your CI can archive:
apidog run \
--access-token "$APIDOG_ACCESS_TOKEN" \
-t <scenarioOrSuiteId> \
-e <environmentId> \
-r cli,html,junit
The -t flag targets a saved scenario, folder, or suite by id. The -e flag selects the environment. The -r flag picks one or more reporters from cli, html, json, and junit. For data-driven runs, add -d (or --iteration-data) with a data file path or test data id. The CLI is headless and runs saved scenarios, so it fits any CI step that can run Node. It’s not a request sender and not a load generator, it runs the functional tests ab was never meant to run.
A healthy setup uses both. Run Apidog scenarios to prove the endpoint is correct. Run ab to prove it’s fast enough. For the correctness side, see the Apidog performance testing guide and the general API performance testing tutorial.
FAQ
Is ApacheBench only for Apache servers?
No. Despite the name, ab sends plain HTTP and HTTPS requests to any server. It works against Nginx, Node, Go, Python, or any host that speaks HTTP. The Apache tie is only that the tool ships with Apache HTTP Server.
What concurrency and request count should I use?
Start low and climb. Try -n 1000 -c 10, then raise -c to 25, 50, 100, and watch where requests per second stops rising and failed requests start appearing. That inflection point is roughly where the endpoint saturates. Match your peak concurrency to expected real traffic rather than picking a large round number.
Why are my failed requests high when the API works fine?
ab flags a request as failed if the response length varies between requests, which is normal for dynamic JSON. Add the -l flag to stop counting length differences as failures. Then check the Non-2xx responses line to see if there are real errors underneath.
Can ab test an endpoint that needs a login token?
Yes, if you already have a token. Pass it with -H "Authorization: Bearer YOUR_TOKEN". What ab can’t do is log in first to obtain a fresh token, then use it. That’s a multi-step flow, and you need a scenario-based tool like Apidog for it.
Does ab support HTTP/2?
No. ab speaks HTTP/1.x and, per the official docs, doesn’t even implement that fully. If your server’s behavior under HTTP/2 matters, use a load tool with HTTP/2 support instead.



