How to check requests per second an api can handle
Start the service on a port you own, then run npx autocannon@8 -c 10 -d 5 http://127.0.0.1:9661/ and read the Avg cell of the Req/Sec row. On the Node server below, that cell held 35 448. Nine further runs against the same server returned 28 277,1 to 39 894,4. One run is not a measurement.
Why check this
Throughput is the number a capacity plan rests on, and the number a release note quotes without saying how it was produced. Run it before any release that changes the request path: a new middleware, a synchronous schema validation, a logging call that writes to disk.
The failure it catches is a handler that became blocking. Response time on one idle request barely moves, because nothing is queued behind it. Throughput halves, and the first sign in production is a queue that stops draining at the busiest hour.
The generator and the target here run on one machine and compete for the same eight cores. That is the honest limit of every figure on this page.
Prerequisites
- Node 22 or later.
npx autocannon@8downloads autocannon on first use. - A free port. Port 9661 is used below. Another process holding it turns the result into a measurement of something else.
- The target, saved as
rps-server.mjs. It answers every path with a small JSON body, and reports its own count on/count.
// rps-server.mjs
import { createServer } from 'node:http';
let served = 0;
createServer((req, res) => {
if (req.url === '/count') {
res.writeHead(200, { 'content-type': 'application/json' });
res.end(JSON.stringify({ served }));
return;
}
served += 1;
res.writeHead(200, { 'content-type': 'application/json' });
res.end('{"ok":true}');
}).listen(9661, '127.0.0.1', () => console.log('listening on 9661'));
- Load goes to a host you own. A generator pointed at somebody else's service is an attack, whatever the intent.
Steps
- Step 1.
Count the listeners on the port before you start.
netstat -ano | grep ":9661 " | grep -c LISTENING0Zero means the port is free. Any other number means a process owns it, and the last column of that line is its process id.
- Step 2.
Start the target and confirm it answers.
node rps-server.mjs & sleep 2 && curl -s -i http://127.0.0.1:9661/ | head -5HTTP/1.1 200 OK content-type: application/json Date: Sat, 12 Sep 2026 08:11:17 GMT Connection: keep-alive Keep-Alive: timeout=5Connection: keep-alivematters. autocannon holds one socket per connection, so a server that closes each response is measured on handshakes rather than on handlers. - Step 3.
Run the load. Ten connections, five seconds.
npx autocannon@8 -c 10 -d 5 http://127.0.0.1:9661/Running 5s test @ http://127.0.0.1:9661/ 10 connections ┌─────────┬──────┬──────┬───────┬──────┬─────────┬─────────┬───────┐ │ Stat │ 2.5% │ 50% │ 97.5% │ 99% │ Avg │ Stdev │ Max │ ├─────────┼──────┼──────┼───────┼──────┼─────────┼─────────┼───────┤ │ Latency │ 0 ms │ 0 ms │ 0 ms │ 0 ms │ 0.01 ms │ 0.19 ms │ 21 ms │ └─────────┴──────┴──────┴───────┴──────┴─────────┴─────────┴───────┘ ┌───────────┬─────────┬─────────┬─────────┬─────────┬─────────┬──────────┬─────────┐ │ Stat │ 1% │ 2.5% │ 50% │ 97.5% │ Avg │ Stdev │ Min │ ├───────────┼─────────┼─────────┼─────────┼─────────┼─────────┼──────────┼─────────┤ │ Req/Sec │ 29 183 │ 29 183 │ 35 743 │ 41 823 │ 35 448 │ 4 052,09 │ 29 182 │ ├───────────┼─────────┼─────────┼─────────┼─────────┼─────────┼──────────┼─────────┤ │ Bytes/Sec │ 5.37 MB │ 5.37 MB │ 6.57 MB │ 7.69 MB │ 6.52 MB │ 745 kB │ 5.37 MB │ └───────────┴─────────┴─────────┴─────────┴─────────┴─────────┴──────────┴─────────┘ Req/Bytes counts sampled once per second. # of samples: 5 177k requests in 5.01s, 32.6 MB readThe throughput figure is in the second table, not the first.
Req/SecAvgis 35 448, and the footer says what it averages: five samples taken once per second.Min29 182 is the slowest second. The last line, 177k requests in 5.01s, is the same fact unaveraged. - Step 4.
Ask the server what it counted.
curl -s http://127.0.0.1:9661/count{"served":177238}177 238 handler calls against the 177k autocannon reported, so the generator counted completed responses rather than attempts. When the two disagree, read the
errors,timeoutsandnon2xxfields before trusting either. - Step 5.
Run the same load at four durations and print the machine-readable average.
for d in 1 5 10 30; do npx autocannon@8 -c 10 -d $d -n -j http://127.0.0.1:9661/ > j-$d.json; node -e "const r=require('./j-$d.json');console.log('-d $d avg',r.requests.average,' min',r.requests.min,' max',r.requests.max,' total',r.requests.total)"; done-d 1 avg 34576 min 34583 max 34583 total 34583 -d 5 avg 32852.81 min 26003 max 40352 total 164237 -d 10 avg 28277.1 min 23649 max 37797 total 311022 -d 30 avg 32401.6 min 19820 max 42155 total 972080One second reports 34 576, ten seconds reports 28 277,1: a 22% spread from one command against one server. The
mincolumn says why. Every run holds a slow second, and a short run gives it more weight. - Step 6.
Look at what the one-second run actually measured.
node -e "const r=require('./j-1.json');console.log('samples',r.samples,' avg',r.requests.average,' min',r.requests.min,' stddev',r.requests.stddev,' total',r.requests.total,' duration',r.duration)"run 1 avg 35280 min 27292 run 2 avg 39849.6 min 33778 run 3 avg 34851.2 min 32177 run 4 avg 34281.6 min 28005 run 5 avg 39894.4 min 34997One sample, so
stddevis 0.Stdev 0reads as a perfectly steady server and means there was nothing to compare. The reported average, 34 576, is not even that sample of 34 583: it is the total over the measured 1.01 s. - Step 7.
Repeat one setting five times and change nothing between runs.
for i in 1 2 3 4 5; do npx autocannon@8 -c 10 -d 5 -n -j http://127.0.0.1:9661/ > r$i.json; node -e "const r=require('./r$i.json');console.log('run $i avg',r.requests.average,' min',r.requests.min)"; donesamples 1 avg 34576 min 34583 stddev 0 total 34583 duration 1.0134 281,6 to 39 894,4 across five identical runs, a 16% band. That is as wide as the spread between the four durations in step 5, so a longer run alone does not buy a stable number. Report the range and the run count behind it.
- Step 8.
Stop the target and confirm the port is clear.
netstat -ano | grep "127.0.0.1:9661 " | grep LISTENING; powershell -Command "Stop-Process -Id 41172 -Force"; netstat -ano | grep ":9661 " | grep -c LISTENINGTCP 127.0.0.1:9661 0.0.0.0:0 LISTENING 41172 0Take the process id from the last column and stop that id alone. Stopping every
node.exetakes other people's servers with it.
How to read the result
| What you see | What it means | What to do |
| --- | --- | --- |
| Req/Sec Avg far above Min | Some seconds were much slower than the rest | Run longer, and add -W [ -c 10 -d 3 ] so warm-up traffic is sent but not sampled. |
| Stdev near Avg | Throughput swung by about its own size | The run measured interference. Repeat it with nothing else on the machine. |
| Stdev exactly 0 | One sample, from a run of one second | Raise -d. A single sample carries no spread to report. |
| non2xx above zero | The server answered, with an error | Error responses are counted in the rate. Fix them before quoting the figure. |
| timeouts above zero | Requests never finished inside 10 s | The rate is now partial. Lower -c and rerun. |
Common mistakes
What to check next
- How to check p95 latency: throughput with no latency distribution beside it hides a forming queue.
- How to test concurrent users: the same server at seven connection counts, and where the curve bends.
- Load test ramp up time: why the first seconds of every run above were slowest.
- How to read load test results: the rest of the table, including the rows that invalidate a run.
- How to check API response time: the single-request figure this one replaces once traffic arrives.
FAQ
What is TPS in performance testing?
Transactions per second counts business operations, and one transaction can be several HTTP calls. Requests per second counts HTTP responses. autocannon reports the second of those: Req/Sec is responses received, sampled once per second.
How do I run a load test with k6?
k6 takes the same shape: a script exporting a default function, vus for connections, duration for the window, then the http_reqs rate in its summary. Every figure here came from autocannon 8.0.0, so no k6 output appears.
How many requests per second can a server handle?
No portable answer exists. The server above returned 28 277,1 to 39 894,4 across ten runs, on one laptop, for a handler that does nothing. Measure your own handler on your own hardware, and publish the run count.
Can I load test a website that is live?
Only one you own, with the owner's agreement and a window fixed in advance. A burst aimed at somebody else's host is an attack. Every command here targets 127.0.0.1.
Verified
Verified by Maks Vernyautocannon 8.0.0node 22.23.2curl 8.21.0
Each output block is what the command above it printed on that date, on the host named in the step. Figures read from a live site move between runs. Compare the shape of the answer rather than the digits, and see the methodology for how a page is re-verified.
Related on this site
intermediate12 minpublished updated Maks Verny