It all starts with one question: what are those users doing? A thousand people logged into an app do not make a thousand requests per second, and a page served from cache costs a hundred times less than a page built on every visit. This guide turns users into requests, requests into CPU and memory, then shows how to check it all with a load test.
"Concurrent users" means three different things depending on who is talking:
You get from one to the other with Little’s law: the number of requests in flight equals the throughput, in requests per second, multiplied by the average time a request takes.
Worked example. 600 users active at once, each triggering a request every 20 seconds, make 30 requests per second. If each request takes 100 ms, the server has only 3 requests in flight on average. The real challenge lies elsewhere: in peaks and in slow requests.
A request holds the CPU for as long as it computes: running the code, querying the database, building the response. One vCPU therefore handles at most 1,000 ÷ t requests per second, where t is the CPU time of a request in milliseconds. Time spent waiting (on an external API, on disk) does not use the CPU, but it does tie up a worker.
| Type of request | Typical CPU time | Per vCPU, order of magnitude |
|---|---|---|
| Static file, or page served from cache | Under 1 ms | Several thousand requests per second |
| WordPress page served by a page cache | 1 to 5 ms | Several hundred per second |
| JSON API (Node.js, Go, FastAPI) with an indexed SQL query | 5 to 20 ms | 50 to 200 per second |
| WordPress or Laravel page built on every visit | 100 to 300 ms | 3 to 10 per second |
| Report, export, PDF generation | 500 ms to several seconds | Fewer than 2 per second |
With these figures, 30 API requests per second at 10 ms of CPU use 30% of a single vCPU. The same 30 requests per second on uncached WordPress pages need 3 to 9 vCPUs. A page cache exists precisely to move a page from the fourth row of this table to the second.
Every request in flight is handled by a worker, and workers take memory whether they are busy or not. It is often memory, more than CPU, that caps how many requests the server handles in parallel:
pm.max_children sets the ceiling: 20 processes means 800 MB to 1.6 GB.max_connections.Persistent connections (websockets, real-time notifications) count separately: they stay open, use little CPU and a few tens of kilobytes of memory each. A Node.js server holds thousands of them in a few hundred MB.
In a real application, the server rarely saturates where you expect. The most common causes, in the order to check them:
pg_stat_statements on PostgreSQL, or the MySQL slow query log, points to it.Orders of magnitude give you a first size; a load test gives you the real one. The idea: simulate more and more users on a copy of the application, and note the point where response times break away.
wrk -t2 -c50 -d30s --latency https://staging.your-site.com/
For a full user journey (log in, browse, search), k6 describes a scenario in JavaScript and ramps the load up in stages:
import http from 'k6/http';
import { sleep } from 'k6';
export const options = {
stages: [
{ duration: '2m', target: 100 },
{ duration: '5m', target: 100 },
{ duration: '2m', target: 300 },
{ duration: '5m', target: 300 },
{ duration: '2m', target: 0 },
],
thresholds: { http_req_duration: ['p(95)<500'] },
};
export default function () {
http.get('https://staging.your-site.com/');
sleep(15);
}
htop for CPU, free -h for memory, the logs for errors.The order that costs least and pays back most:
The first three steps cover the vast majority of business applications, websites and APIs: several hundred users active at the same time fit on a single well-tuned server.
VPS Pro comes in six sizes, on Intel Xeon processors, ECC memory and mirrored NVMe SSDs, and moves up to a larger size at any time without reinstalling. Full-Stack Assistance is included: tell us how many users you expect at launch and a year from now and we recommend the size; and when a page slows down, we diagnose it and tell you what to do, from the operating system down to your application code.
For an API or an app whose requests take 10 to 20 ms of CPU, several hundred users active at the same time. For a WordPress site without a page cache, around a hundred. The gap comes from the CPU time of each request and from caching, which is why a load test is worth running.
Daily visitors say little on their own: what counts is the peak. To convert one into the other, divide the day’s visits by the active hours, multiply by pages per visit, then by a peak factor of 3 to 5. 10,000 visits over 10 hours at 4 pages each give about 1 page per second on average, and 3 to 5 at peak.
Three signs: response time rises with traffic, CPU stays above 80% at busy times, or available memory drops close to zero. The web server logs then show 502 or 504 errors, and requests queue up waiting for a free worker.
Yes: a websocket connection stays open all the time. It uses little CPU and a few tens of kilobytes of memory. A Node.js server holds thousands of them; it is the messages sent, more than the open connections, that load the CPU.
Simafri
Let's talk about your website
Tell us about your project in a few words: we will get back to you quickly.
Thank you! Your request has been sent. We'll get back to you shortly.
Prefer email? Write to us at support@simafri.com.