Open your website, and it loads in a blink. But that is one visitor on an empty server. The real question is what happens when a newsletter goes out, a post goes viral or a sale starts - and hundreds of people open the same pages at the same moment.
A load test answers exactly that: it sends many requests at once and shows how the server copes. In this guide we explain the few numbers that matter, run a real experiment and walk through the steps to test your own website.
We took a small test stack: nginx in front of a PHP application (Laravel) with PHP-FPM limited to 5 workers - a common setting on small servers and the default of many Docker images. On it we tested two URLs:
Each run sent 1,000 requests with ApacheBench (the same engine Siteload uses), and we raised the concurrency step by step: 1, 2, 5, 10, 25, 50 and 100.
A fair note: the load generator ran on the same machine as the server, so the absolute numbers are lower than on a dedicated server. The shape of the curves is what matters - and you will see the same shape on any server.
The dynamic page grows from 55 to 167 requests per second - and stops exactly at 5 concurrent requests. 5 is the number of PHP workers. From that point on, adding visitors doesn't add throughput: every worker is busy, and new requests wait in a queue.
And the queue is visible in the response time. At 100 concurrent requests the slowest 5% of visitors waited 689 ms instead of 23 ms - 30 times longer, although the server did exactly the same work per request. The math is simple: 100 requests in the queue divided by 166 requests per second is 0.6 seconds of waiting, which is what we measured.
| Concurrent requests | Dynamic, req/s | Dynamic, p95 | Static, req/s | Static, p95 |
|---|---|---|---|---|
| 1 | 55 | 23 ms | 574 | 2 ms |
| 2 | 103 | 23 ms | 793 | 3 ms |
| 5 | 167 | 42 ms | 1,482 | 5 ms |
| 10 | 167 | 76 ms | 1,892 | 9 ms |
| 25 | 165 | 174 ms | 2,063 | 18 ms |
| 50 | 163 | 352 ms | 2,073 | 36 ms |
| 100 | 166 | 689 ms | 2,061 | 62 ms |
1,000 requests per run, no failed requests and no non-2xx responses in any run.
The static file reached about 2,000 requests per second - 12 times more - because nginx serves it without waiting for PHP.
Look at the table again: zero errors in every run. Every request got a 200 response. A monitor that only checks if the website is up would report that everything is fine - while every visitor waits several times longer than usual.
That is why the response time percentiles are the main result of a load test, not the error count. Errors come much later: when the queue grows longer than the web server is ready to wait (a 502 or 504 from nginx), or when the timeouts of the visitors' browsers run out.
After the test we found something we didn't measure: the sessions table had 7,003 new rows. The application stores sessions in the database, and every anonymous request - every visitor without a cookie - started a new session and wrote it to the database.
Bots and crawlers don't keep cookies either, so they do exactly the same on a live website. Such side effects (sessions, logs, cache entries, analytics events) are invisible on one request and become a problem under load. After a load test, check what grew in your database and on disk.
Every dynamic website has a ceiling, and on a typical PHP server it is set by the number of workers. Below it, the website is fast. Above it, nothing breaks - the website just gets slower with every new visitor, and no error tells you about it.
A load test finds that ceiling in a few minutes. Then the fix is usually simple: cache the pages that don't change, serve them without PHP, or give the server more workers if it has the memory and CPU for them.
Run a free load test with up to 1,000 concurrent connections, see the response time of every request and compare the results after each change.
Learn about load testingAnd to know when the website stops responding at all, set up uptime monitoring - it emails you when it goes down and when it is back online.