PERFORMANCE
August 5, 2026

How to Test How Many Visitors Your Server Can Handle

7 min read
Author
CloudStick Team
Server Infrastructure
Share this article
How to Test How Many Visitors Your Server Can Handle
CloudStick
Find Your
True Ceiling

Requests Per Second Vs Concurrent Visitors

Every load testing tool reports requests per second, but that is not the number a marketing person actually wants when they ask "how much traffic can we handle for the launch." Requests per second measures how fast your server can churn through work when a benchmark fires requests back to back with zero human pauses in between. A real visitor does not behave that way — they load a page, read it, click something, wait, then move on. The gap between those two behaviors is exactly why a server that survives a synthetic 500 req/sec benchmark can still fall over during a launch with only 300 real concurrent people on it.

To translate one into the other you need two extra numbers: average session duration and average pages viewed per visit. If a visitor stays on the site for 3 minutes and views 6 pages, each page request is spaced roughly 30 seconds apart for that one person. A server handling 10 requests per second at a comfortable response time can, very roughly, sustain 10 x 30 = 300 concurrent visitors browsing at that pace, since each one only occupies a request slot for a fraction of a second at a time rather than continuously. This is a rough estimate, not a guarantee — it ignores static assets, background AJAX calls, and bursty behavior — but it is far closer to the truth than quoting the raw requests-per-second number as if it were a visitor count.

Never Load Test Production Directly

Build a staging clone of the site on the same server class you actually run in production — same PHP version, same PHP-FPM pool settings, same MariaDB configuration, ideally seeded with a realistic amount of data rather than an empty database. Testing against an empty database will drastically understate how slow queries get once tables have real row counts, and testing against a smaller server than production will give you a ceiling number that has nothing to do with the box your visitors actually hit.

WARNING

Do not point wrk, k6, or siege at a live production URL outside of a controlled maintenance window. A ramping load test is specifically designed to push a server past its breaking point, and if that server is serving real customers, you will cause the exact outage you were trying to prevent. If you must test against production to catch environment-specific issues a clone can't reproduce, schedule a low-traffic window, warn your team, and be ready to kill the test immediately if error rates spike on real requests.

Run the load generator from a separate machine, not from the server under test — otherwise the benchmark tool competes with the web server for the same CPU and network bandwidth, and you end up measuring the load generator's limits instead of the server's. A small $5 VPS in the same region as your target server is usually enough to generate meaningful load without becoming the bottleneck itself.

Ramping Load With wrk And k6

Don't jump straight to your expected peak concurrency. Ramp up gradually and record response time and error rate at each step, because the number that matters is not "how many requests per second did it do" but "at what concurrency did response time or errors start climbing." That inflection point is the real capacity, and it is almost always well below the point where the server stops responding entirely.

# wrk: 12 threads, ramp connections in steps, 30s per step
wrk -t12 -c50 -d30s --latency https://staging.example.com/
wrk -t12 -c150 -d30s --latency https://staging.example.com/
wrk -t12 -c300 -d30s --latency https://staging.example.com/
wrk -t12 -c600 -d30s --latency https://staging.example.com/

k6 does the same ramp in a single script and adds proper scripted user journeys — logging in, browsing a category page, viewing a product — which gets you closer to real visitor behavior than hammering a single URL:

// loadtest.js
import http from "k6/http";
import { sleep } from "k6";
export const options = {
stages: [
{ duration: "1m", target: 50 },
{ duration: "2m", target: 150 },
{ duration: "2m", target: 300 },
{ duration: "1m", target: 0 },
],
};
export default function () {
http.get("https://staging.example.com/");
sleep(Math.random() * 3 + 2); // simulate a real reading pause
}
k6 run loadtest.js

That sleep() call is the difference between a synthetic hammer test and a plausible visitor simulation — it keeps each virtual user's request rate closer to how an actual person browses, so the concurrency number k6 reports lines up much better with a real concurrent-visitor count.

Watching PHP-FPM And MySQL Limits

Response times don't degrade smoothly — they usually hold flat and then fall off a cliff the moment a hard limit gets hit. The most common cliff on a PHP site is the PHP-FPM pool's pm.max_children setting: once every worker process is busy handling a request, new requests queue in Apache's or PHP-FPM's backlog, and once that backlog fills too, Nginx starts returning 502 Bad Gateway to visitors even though the server still has free CPU and RAM sitting idle.

tail -f /home/<username>/logs/<sitename>/php-fpm/error.log
# look for: "server reached pm.max_children (X) ... consider raising it"
mysql -e "SHOW STATUS LIKE 'Threads_connected'; SHOW VARIABLES LIKE 'max_connections';"

MariaDB has the same kind of wall in max_connections: once Threads_connected reaches that ceiling, new connection attempts get rejected outright, which shows up in the PHP error log as "Too many connections" long before the server's CPU or RAM looks stressed. This is why a load test that only watches top or htop can mislead you — a server can be sitting at 40% CPU and still be completely unable to serve a new visitor, because the bottleneck is a connection count, not a compute resource.

On a CloudStick server you don't need three SSH tabs open to correlate these signals — the dashboard's Server panel shows live CPU, RAM, and disk graphs pulled from the Zabbix Agent 2 running on the box, so while a ramp step is running in your terminal you can watch resource saturation happen in real time on screen instead of guessing at it after the test finishes and the numbers have already settled back down.

Reading Results And Estimating Real Capacity

Pull the p95 latency and error rate at each concurrency step from wrk's --latency output or k6's summary, and plot them against concurrency in your head or a spreadsheet. You are looking for the concurrency value where p95 latency roughly doubles from its baseline, or where the error rate first crosses above zero — whichever comes first. That value, not the concurrency where the server finally stops responding altogether, is the number you report as the site's real capacity.

Concurrency p95 latency Error rate PHP-FPM pool MySQL conns
50 110ms 0% 12/40 busy 18/151
150 140ms 0% 34/40 busy 46/151
300 410ms 2% 40/40 busy 89/151
600 timeouts 31% 40/40 busy 151/151 (refused)

In that example, the real ceiling is around 150-250 concurrent connections, not the 600 the test attempted — that's where pm.max_children first saturates and latency starts climbing. Take that concurrent-connection ceiling and run it back through the session-duration math from the first section: if your average visitor holds a connection open (actively loading something) for roughly 2 seconds out of every 30-second browsing interval, a ceiling of 200 concurrent connections translates to something on the order of 200 x 15 = 3,000 concurrently browsing visitors before things degrade. Adjust that multiplier for your own site's real average session behavior from analytics rather than assuming ours.

Summary

Clone the site to staging, never fire a ramping load test at live production outside a planned window, and use wrk or k6 to step concurrency up gradually rather than jumping straight to your target number. Watch pm.max_children and MariaDB's max_connections alongside CPU and RAM — on CloudStick that means keeping the dashboard's live resource graphs open in one window while the test runs in another — because the first hard limit you hit, not raw requests per second, is what defines the server's actual ceiling. Convert that concurrent-connection ceiling into a concurrent-visitor estimate using your site's real average session duration and pages per visit, and you'll have a number worth reporting before a launch instead of one you're guessing at during an outage.

Leave a comment
Full Name
Email Address
Message
Contents