Testing from your own machine
The built-in load test runs oha on the server it is testing, alongside the application and the database. That keeps an A and B comparison clean, because the request path is identical between your two runs. It also costs you two things, and both are fixable by driving the load yourself from somewhere else.
You want the absolute number. With the load generator on the box, some of the CPU that could have served requests is generating them instead. Move it off and the server has the whole machine for the thing you are measuring.
You want honest tail latency. The built-in test holds a fixed number of connections open. When the server slows down, a fixed-connection client waits rather than sending more, so the reported p95 and p99 come out better than reality. BenchKit labels its own tail percentiles as indicative for that reason.
Get the commands
BenchKit builds the commands for you, already pointed at your server.
Open the app
Go to the BenchKit start screen.
Find the link under the start button
Press Prefer your own tools? You can run some tests externally too.
Copy a command
The Test from your own machine modal lists each endpoint with a ready-to-run command. Every command carries your server's address, so there is nothing to fill in.
Install oha
oha is the same load generator BenchKit uses internally, which is what keeps the two comparable.
# macOS
brew install oha
# Cargo, on anything else
cargo install oha
Install it on the machine driving the load, not on the BenchKit server.
The endpoints
These four routes are the same targets the built-in test drives. They skip session and CSRF middleware, so any machine that can reach the server can load test them.
| Endpoint | What it represents |
|---|---|
/bench/static | Framework baseline. A static response, no database |
/bench/json | An API response. A 25-item JSON payload built in PHP |
/bench/db-read | A database read. 20 rows queried per request |
/bench/io?ms=100 | Simulated I/O. Sleeps for the milliseconds you pass, standing in for one outbound call |
ms is clamped to the 0–1000 range the settings enforce, so a stray query string cannot tie up a worker.
Measuring throughput
This is the closed-loop run: hold 50 connections open for 30 seconds and see how much the server gets through.
oha -z 30s -c 50 http://<your-server-ip>/bench/db-read
Swap the path for any endpoint in the table. Raise -c to push concurrency higher.
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.Measuring tail latency
A fixed-connection run reports throughput well and understates p99. Gil Tene named this coordinated omission. To measure latency honestly, drive a constant request rate instead and let oha correct for it.
Do it in two passes:
Find your maximum throughput
Run the throughput command above and note the requests per second it reports.
Run again at about 70% of that
Pass the rate with -q and add --latency-correction.
oha -q <req_per_sec> -z 30s --latency-correction http://<your-server-ip>/bench/db-read
--latency-correction does nothing without a target rate. The -q flag is what makes it work, which is why the two always appear together.Staying below your maximum is the point. At saturation every client queues, and the numbers stop describing latency and start describing the backlog.
Reading what you get back
External numbers are not built-in numbers. An external run includes network time between the two machines and keeps the load generator off the server. Expect different figures than the Web Server Load Test panel, and do not put the two side by side as though they measured the same thing.
Your network is now part of the measurement. Drive the load from a machine close to the server when you want to measure the server. Latency from a laptop three regions away is mostly the internet.
Run each endpoint on its own. The built-in test measures one route at a time for the same reason. Overlapping runs compete and neither result means much.
/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 web server load test enabled once before you drive that endpoint from outside. The other three endpoints work immediately./bench/env, which reports what PHP looks like in the process serving requests rather than in the command line process. It is not a load target. BenchKit calls it to record the environment on your results, and it is worth a look when you want to confirm which settings were actually in effect.Keeping the comparison fair
Everything in Reading your results still applies. Change one thing at a time, run each variation on the same hardware, and drive the load from the same client machine every time. A result that moved because you switched laptops is not a result.