Benchmarks & Methodology

Testing from another machine

Pair a second machine to drive the load test, or point oha at the endpoints yourself.

For the most accurate results, you want to use a second machine because when you use your CPU to generate and serve traffic, you can cause your numbers to be skewed.

It's recommended to use a separate machine to help you get the most accurate results. You'll need:

  • A machine near your server, ideally in the same datacenter or region.
  • oha installed on it.

Pick the machine

When you're performing tests, it's important to minimize any variables between the two machines.

Where your 2nd machine isWhat you end up measuring
Same datacenter or regionYour server. This is what you want
Same LAN, or a VM on the same hostYour server, mostly. A VM still shares the host's CPU
Another region or providerThe network between the two machines
A laptop on wifiYour wifi speeds

Two more things to consider:

  • Give it enough CPU. A single-core box runs out of headroom before your server does. Match it roughly to the machine under test.
  • Don't let it sleep. A laptop that sleeps mid-run takes the stage down with it.

Install oha (on the generator machine)

Run these commands on the machine you want to simulate traffic from.

Terminal
brew install oha

BenchKit looks in ~/.local/bin and ~/.cargo/bin as well as your PATH.

Skip this step and nothing breaks. The pairing command stops and prints the exact download line for that machine's platform. It never installs anything itself.

Run the pairing command

Start a run with the web server load test enabled. BenchKit shows you a command:

Terminal
curl -kfsSL https://your-benchkit-instance/bench/generator/<token>/script | sh

Paste it on the generator machine. It connects, the run starts, and the load test runs first so your part is over early.

Each route claims a line and fills in the throughput it measured:

  1/4  /bench/static       12,483 req/s  ✓
  2/4  /bench/json          8,120 req/s  ✓
  3/4  /bench/db-read       4,902 req/s  ✓
  4/4  /bench/io              487 req/s  ✓

Leave the terminal open until it finishes. Closing it stops the load test.

If it doesn't connect

MessageFix
oha is not installed on this machineInstall it. The script prints the exact download line for that machine
Could not reach <url> from this machineCheck the URL and any firewall between the two machines
The server did not accept the handshakeThe pairing expired or was replaced. Start the run again for a fresh command
This pairing is no longer validThe run finished or was cancelled while you waited. Start again
A route ends in The server refused that upload and prints why on the same line

The generator needs to reach the URL you have open in your browser, on /bench/*. Nothing needs to be open in the other direction.

If BenchKit is only reachable on loopback or through an SSH tunnel, your browser's URL is no use to a second machine. Set BENCHMARK_HTTP_URL to an address the generator can reach.

Driving oha yourself

The benchmark routes are open endpoints with session and CSRF middleware skipped, so any machine that can reach the server can point oha at them.

Results driven this way are not recorded into a run. Use this for ad-hoc probing, and for tail latency, which no fixed-connection test can measure honestly.

EndpointWhat it represents
/bench/staticFramework baseline. No database
/bench/jsonAn API response. A 25-item JSON payload built in PHP
/bench/db-readA database read. 20 rows per request
/bench/io?ms=100Simulated I/O. Sleeps for the milliseconds you pass

Throughput

Hold a set number of connections open and see what the server gets through:

Terminal
oha -z 30s -c 50 http://<your-server-ip>/bench/db-read

Run one endpoint at a time. Overlapping runs compete and neither result means much.

Under PHP-FPM a request holds a worker for its whole duration. If your connection count is above pm.max_children, you are measuring the pool rather than the server. Raise PHP_FPM_PM_MAX_CHILDREN to at least your core count first. See Customizing the image.

Tail latency

A fixed-connection run understates p99: when the server slows down, the client waits instead of sending more, so the requests that would have been slowest are never sent. Gil Tene named this coordinated omission.

A paired run already does this for you. The response times on your result come from a separate pass at a held rate below the maximum, with the correction applied — that is what the three measurements are for. What follows is how to do it by hand against your own routes.

Two passes:

Find your maximum throughput

Run the throughput command above at a few connection counts and note the highest requests per second. One connection count is one point on a curve, and the maximum is rarely at the first number you try.

Run again at about 70% of that

Terminal
oha -q <req_per_sec> -z 30s --latency-correction http://<your-server-ip>/bench/db-read

Staying below your maximum is the point. At saturation every client queues, and the numbers describe the backlog rather than latency.

--latency-correction does nothing without a target rate. The -q flag is what makes it work, which is why the two always appear together.

Reading what you get

Numbers you drive by hand are not the numbers in the Web Server Load Test panel. Yours include network time and whatever flags you passed. Don't compare the two directly. For an external number that is comparable, pair the machine with a run.

/bench/db-read answers 503 until the table it queries exists. That table is prepared when the HTTP stage starts, so run a benchmark with the load test enabled once before driving that endpoint from outside. The other three work immediately.
A fifth route, /bench/env, reports what PHP looks like in the process serving requests rather than in the CLI process. It is not a load target. BenchKit calls it to record the environment on your results.

When a self-test is fine

A self-test runs oha on the BenchKit server itself. No second machine, no setup.

It is the right tool for comparing two configurations on the same box, because the request path is identical between your runs. What it can't give you is the absolute number, since the generator competes with what it measures. Treat a self-test figure as a floor.

Every result records which mode produced it, and the two are never compared on one axis. See Reading your results for what else applies.