How many users can your server handle before pages slow down or errors appear? Load testing answers that before a launch, campaign or traffic spike does. k6 is a modern, open-source load testing tool: you write tests in JavaScript, it runs them efficiently from one machine, and it reports latency percentiles, error rates and throughput.
Install
sudo gpg -k
sudo gpg --no-default-keyring --keyring /usr/share/keyrings/k6-archive-keyring.gpg --keyserver hkp://keyserver.ubuntu.com:80 --recv-keys C5AD17C747E3415A3642D57D77C6C491D6AC1D69
echo "deb [signed-by=/usr/share/keyrings/k6-archive-keyring.gpg] https://dl.k6.io/deb stable main" | sudo tee /etc/apt/sources.list.d/k6.list
sudo apt update && sudo apt install k6
# or: docker run --rm -i grafana/k6 run - <script.js
A first test
// load.js
import http from "k6/http";
import { check, sleep } from "k6";
export const options = {
stages: [
{ duration: "1m", target: 20 }, // ramp up to 20 virtual users
{ duration: "3m", target: 20 }, // stay
{ duration: "1m", target: 100 }, // push harder
{ duration: "3m", target: 100 },
{ duration: "1m", target: 0 }, // ramp down
],
thresholds: {
http_req_failed: ["rate<0.01"], // < 1% errors
http_req_duration: ["p(95)<800"], // 95% of requests under 800 ms
},
};
export default function () {
const res = http.get("https://staging.example.com/");
check(res, { "status 200": (r) => r.status === 200 });
sleep(1); // think time between page views
}
k6 run load.js
Reading the results
Key lines in the summary:
| Metric | Meaning |
|---|---|
http_req_duration p(90), p(95) |
Latency for most users. Watch percentiles, not the average |
http_req_failed |
Share of failed requests (non-2xx/3xx, timeouts) |
http_reqs |
Requests per second achieved |
vus |
Concurrent virtual users |
| ✓/✗ thresholds | Pass/fail against your targets |
While the test runs, watch the server: CPU, memory, PHP-FPM or Node workers, and database load (htop, vmstat 1, Grafana). The first resource to saturate is your bottleneck. See VPS performance troubleshooting.
Realistic scenarios
Real users don’t all hit the home page. Model the main journeys:
export default function () {
http.get("https://staging.example.com/");
sleep(2);
http.get("https://staging.example.com/products/vps-plan");
sleep(3);
const login = http.post("https://staging.example.com/api/login",
JSON.stringify({ email: `user${__VU}@test.local`, password: "test" }),
{ headers: { "Content-Type": "application/json" } });
check(login, { "logged in": (r) => r.status === 200 });
sleep(2);
}
- Include uncached paths (search, cart, logged-in pages). Cached pages are easy. Dynamic ones show real limits.
- Use test accounts and data. Don’t create real orders or send real emails.
- Other test types: spike (sudden jump), soak (moderate load for hours, to find memory leaks), breakpoint (keep increasing until it breaks).
Where to run it from
- Not from the server under test: the load generator would compete for CPU.
- A separate VPS (ideally in a location near your real users), or several, for big tests. k6 handles thousands of virtual users from one modest machine.
- Test staging that mirrors production, or production during a quiet window, with care.
Only load-test systems you own or have permission to test. Heavy tests against third-party services or your provider’s shared infrastructure can look like an attack. Rate limits and Cloudflare may block the test too. Allowlist the generator’s IP, or test the origin directly.
Act on the results
Typical fixes when limits are low: page and object caching, more PHP-FPM or Node workers (if RAM allows), database indexes and tuning, offloading static files to a CDN, or a bigger plan. Then test again. See WordPress TTFB, Node.js cluster mode, and when to upgrade your VPS.
Quick reference
| Task | How |
|---|---|
| Run | k6 run script.js |
| Ramp users | options.stages |
| Pass/fail targets | thresholds (p95 latency, error rate) |
| Quick constant load | k6 run --vus 50 --duration 5m script.js |
| Watch server | htop, vmstat, Grafana during the test |
Related: wrk load testing and web performance testing.