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

// 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'));

Steps

  1. Step 1.

    Count the listeners on the port before you start.

    netstat -ano | grep ":9661 " | grep -c LISTENING
    
    0

    Zero means the port is free. Any other number means a process owns it, and the last column of that line is its process id.

  2. 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 -5
    
    HTTP/1.1 200 OK
    content-type: application/json
    Date: Sat, 12 Sep 2026 08:11:17 GMT
    Connection: keep-alive
    Keep-Alive: timeout=5

    Connection: keep-alive matters. autocannon holds one socket per connection, so a server that closes each response is measured on handshakes rather than on handlers.

  3. 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 read

    The throughput figure is in the second table, not the first. Req/Sec Avg is 35 448, and the footer says what it averages: five samples taken once per second. Min 29 182 is the slowest second. The last line, 177k requests in 5.01s, is the same fact unaveraged.

  4. 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, timeouts and non2xx fields before trusting either.

  5. 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 972080

    One second reports 34 576, ten seconds reports 28 277,1: a 22% spread from one command against one server. The min column says why. Every run holds a slow second, and a short run gives it more weight.

  6. 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 34997

    One sample, so stddev is 0. Stdev 0 reads 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.

  7. 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)"; done
    
    samples 1  avg 34576  min 34583  stddev 0  total 34583  duration 1.01

    34 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.

  8. 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 LISTENING
    
      TCP    127.0.0.1:9661         0.0.0.0:0              LISTENING       41172
    0

    Take the process id from the last column and stop that id alone. Stopping every node.exe takes 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

Sign: A script that parses the printed average gets 35 instead of 35448.Cause: autocannon formats the table with Intl.NumberFormat in the machine locale. Here the average printed as 35 448 with a non-breaking space for thousands, and a decimal comma in 34 281,6. parseFloat stops at the space. Read numbers from -j output, where requests.average is a plain JSON number.
Sign: A warm-up run with JSON output crashes JSON.parse, or reports a figure nobody recognises.Cause: Combining -W and -j writes two JSON documents on two lines: the warm-up first, then the measured run. One capture here held 13708 followed by 19836. JSON.parse throws on the pair and head -1 silently reads the warm-up. Take the last line.
Sign: The figure is quoted as what the server can handle.Cause: The generator ran on the same machine as the target and took cores from it. What was measured is the pair. Such a number is a floor for the server and a ceiling for the two together, and it belongs in a release note with the words on one machine attached to it.

What to check next

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.

intermediate12 minpublished updated Maks Verny