☁️VoltTest Cloud closed beta is now openJoin the waitlist
Skip to main content

PHP Load Testing: How to Load Test PHP Applications

· 12 min read
Islam A-Elwafa
Software Engineer

PHP powers roughly 77% of all websites with a known server-side language — yet most PHP developers ship code without ever running a load test. The app works fine in development, handles a handful of users on staging, and then crumbles when real traffic arrives. Load testing catches that gap before your users do: it verifies that your application handles the traffic you actually expect, not just the traffic your laptop can produce.

This guide covers what load testing means for PHP, how to pick the right tool, how the main options compare, and how to write and run a real multi-step load test — on plain PHP or Laravel — in minutes.

PHP Load Testing

What Is Load Testing?

Load testing simulates a realistic number of concurrent users hitting your application to verify it handles normal and peak traffic without degrading. It answers questions like: Can the app serve 500 users at once without response times climbing past 200ms? Does the database connection pool hold up under sustained load? Do sessions and caches behave correctly when 100 users are active simultaneously?

It's easy to mix up the different flavors of performance testing:

  • Load testing — verify the app handles an expected level of traffic (e.g. 500 concurrent users for 10 minutes).
  • Stress testing — push past expected traffic to find the breaking point. (For a hands-on guide, see PHP Stress Testing Tool.)
  • Benchmarking — measure raw throughput of a single endpoint (ab -n 1000 -c 50), without modeling realistic user flows.

Load testing is the baseline: it tells you whether your app can handle Tuesday at 2pm, not just whether it survives Black Friday.

Why Load Testing Matters Specifically for PHP

PHP's request lifecycle introduces concerns that don't exist in long-running runtimes like Node.js or Go:

  • Process-per-request model — each request bootstraps the framework. Under load, this means OPcache warming, autoloader performance, and memory limits all become bottlenecks.
  • Database connection pooling — PHP doesn't keep connections alive across requests by default. Under load, the connection churn can exhaust your database's max_connections.
  • Session handling — file-based sessions create lock contention when multiple requests hit the same user session. Load testing surfaces this before production does.
  • External service timeouts — a payment gateway that responds in 200ms under light load might take 2 seconds when 50 requests queue up. Your timeout settings need to account for this.

When Should You Load Test a PHP Application?

You don't need to load test every commit. But you should run one:

  • Before major releases — especially ones that change database queries, caching, or authentication flows.
  • After infrastructure changes — new server, PHP version upgrade, switching from Apache to Nginx/FrankenPHP, moving to containers.
  • Before expected traffic spikes — product launches, marketing campaigns, seasonal peaks.
  • When adding heavy features — file uploads, report generation, real-time notifications, anything that changes how the app uses CPU or I/O.
  • In CI/CD — catch performance regressions automatically. VoltTest's PHPUnit integration makes this straightforward.

What to Look For in a PHP Load Testing Tool

Not every load testing tool fits a PHP team. Before picking one, check it against these criteria:

  • PHP-native test definition — can you write tests in PHP and install via Composer, or do you have to learn a separate language and toolchain?
  • Realistic multi-step scenarios — real users don't just hit one endpoint. They browse, log in, add to cart, and check out. The tool should chain requests, pass data between steps, and handle cookies/tokens.
  • Concurrent virtual users — true concurrency, not sequential requests fired in a loop.
  • Percentile metrics — averages hide problems. You need P95 and P99 latency to see what your slowest users experience.
  • Data-driven testing — CSV data sources so each virtual user gets unique credentials, avoiding session contention.
  • Scalability — local testing for development, cloud execution for production-scale validation.

PHP Load Testing Tools Compared

Here's an honest comparison of the most common options:

FeatureVoltTestApache JMeterk6 (Grafana)LocustApache Bench
Write tests inPHPJava / XML GUIJavaScriptPythonCLI flags
Install viaComposerDownload JVM appInstall binarypip installPre-installed (most OS)
Laravel integrationNative packageNoneNoneNoneNone
Multi-step scenariosYesYesYesYesNo
Concurrent VUs (local)ThousandsHundredsThousandsHundredsLimited
Cloud scalingBuilt-inDIY infrastructureGrafana CloudDIY infrastructureN/A
P95/P99 metricsYesYesYesYesNo
Data-driven (CSV)YesYesYesYesNo
CI/CD integrationPHPUnitJenkins pluginCLICLICLI
  • Apache Bench (ab) is useful for a quick one-off sanity check (ab -n 1000 -c 50 http://localhost/api/health), but it can't model user flows, extract tokens, or report percentile latencies.
  • JMeter is the established enterprise option with broad protocol support, but its XML/GUI workflow is a steep learning curve for a team that just wants to validate their PHP API.
  • k6 is modern and well-documented, but you write tests in JavaScript — a context switch for a PHP team.
  • Locust follows the same pattern but in Python.
  • VoltTest is built for PHP and Laravel teams: tests are written in PHP, installed with Composer, and executed by a Go engine that handles the concurrent load generation. It's the only option with a native Laravel package, Artisan commands, and PHPUnit integration.

Load Testing a PHP Application with VoltTest

Let's write a real load test. The core SDK is framework-agnostic — it works with any PHP application.

Install

composer require volt-test/php-sdk

Basic Load Test — Single Endpoint

Start simple: 50 concurrent users hitting an API endpoint for 30 seconds.

load-test.php
<?php
require 'vendor/autoload.php';

use VoltTest\VoltTest;

$test = new VoltTest('API Load Test');

$test->setVirtualUsers(50)
->setRampUp('10s')
->setDuration('30s');

$scenario = $test->scenario('Product Listing');
$scenario->step('Get Products')
->get('https://your-app.test/api/products')
->header('Accept', 'application/json')
->validateStatus('products_ok', 200);

$test->run(true);

Run it:

php load-test.php

The setRampUp('10s') gradually adds virtual users over 10 seconds instead of slamming the app with all 50 at once — this models realistic traffic, not a thundering herd.

Multi-Step Load Test — Realistic User Flow

Real users don't just hit one endpoint. Here's a load test that simulates browsing products, logging in, and placing an order:

checkout-flow-test.php
<?php
require 'vendor/autoload.php';

use VoltTest\VoltTest;
use VoltTest\DataSourceConfiguration;

$test = new VoltTest('Checkout Flow Load Test');

$test->setVirtualUsers(30)
->setRampUp('15s')
->setDuration('2m');

$scenario = $test->scenario('Browse → Login → Checkout')
->autoHandleCookies();

// Each VU gets a unique user from the CSV
$scenario->setDataSourceConfiguration(
new DataSourceConfiguration(__DIR__ . '/users.csv', 'unique', true)
);

// Step 1: Browse products
$scenario->step('List Products')
->get('https://your-app.test/api/products')
->header('Accept', 'application/json')
->extractFromJson('product_id', 'data[0].id')
->validateStatus('products_loaded', 200)
->setThinkTime('2s');

// Step 2: Log in and extract auth token
$scenario->step('Login')
->post('https://your-app.test/api/login',
'{"email":"${email}","password":"${password}"}')
->header('Content-Type', 'application/json')
->header('Accept', 'application/json')
->extractFromJson('token', 'data.token')
->validateStatus('login_ok', 200)
->setThinkTime('1s');

// Step 3: Add product to cart
$scenario->step('Add to Cart')
->post('https://your-app.test/api/cart',
'{"product_id":"${product_id}","quantity":1}')
->header('Authorization', 'Bearer ${token}')
->header('Content-Type', 'application/json')
->header('Accept', 'application/json')
->validateStatus('added_to_cart', 200)
->setThinkTime('2s');

// Step 4: Checkout
$scenario->step('Checkout')
->post('https://your-app.test/api/checkout', '')
->header('Authorization', 'Bearer ${token}')
->header('Content-Type', 'application/json')
->header('Accept', 'application/json')
->validateStatus('checkout_ok', 201);

$result = $test->run(true);

echo "\n\nResults:\n";
echo "Total Requests: " . $result->getTotalRequests() . "\n";
echo "Success Rate: " . $result->getSuccessRate() . "%\n";
echo "RPS: " . $result->getRequestsPerSecond() . "\n";
echo "P95 Latency: " . $result->getP95ResponseTime() . "\n";
echo "P99 Latency: " . $result->getP99ResponseTime() . "\n";

The CSV file supplies unique credentials so each virtual user logs in with different data — no session contention:

users.csv
email,password
user1@example.com,password123
user2@example.com,password123
user3@example.com,password123

Load Testing a Laravel Application

If you're on Laravel, the dedicated package adds Artisan commands, automatic CSRF handling, and PHPUnit integration on top of the core SDK.

Install

composer require volt-test/laravel-performance-testing --dev
php artisan vendor:publish --tag=volttest-config

Quick Load Test from the Command Line

For a fast check, test a single URL directly from Artisan — no test class needed:

php artisan volttest:run https://your-app.test/api/products --users=50 --duration=30s

This gives you the one-liner speed of Apache Bench but with real virtual-user concurrency and percentile metrics.

Full Scenario Load Test

Scaffold a test class:

php artisan volttest:make CheckoutLoadTest

Define the scenario with CSRF token handling and data sources:

app/VoltTests/CheckoutLoadTest.php
<?php

namespace App\VoltTests;

use VoltTest\Laravel\Contracts\VoltTestCase;
use VoltTest\Laravel\VoltTestManager;

class CheckoutLoadTest implements VoltTestCase
{
public function define(VoltTestManager $manager): void
{
$manager->target('http://localhost:8000');

$scenario = $manager->scenario('Web Checkout Flow')
->dataSource('users.csv', 'unique');

$scenario->step('Visit Shop')
->get('/shop')
->expectStatus(200)
->extractCsrfToken()
->thinkTime('2s');

$scenario->step('Add to Cart')
->post('/cart/add', [
'_token' => '${csrf_token}',
'product_id' => 1,
'quantity' => 1,
])
->expectStatus(302)
->thinkTime('1s');

$scenario->step('Checkout Page')
->get('/checkout')
->expectStatus(200)
->extractCsrfToken()
->thinkTime('3s');

$scenario->step('Place Order')
->post('/checkout', [
'_token' => '${csrf_token}',
'email' => '${email}',
])
->expectStatus(302);
}
}

Run it:

php artisan volttest:run CheckoutLoadTest --users=100 --duration=2m

Load Testing in PHPUnit

You can also run load tests inside your existing PHPUnit suite and fail the build when performance degrades:

tests/Performance/CheckoutLoadTest.php
<?php
namespace Tests\Performance;

use App\VoltTests\CheckoutLoadTest as VoltCheckout;
use VoltTest\Laravel\Testing\PerformanceTestCase;

class CheckoutLoadTest extends PerformanceTestCase
{
public function testCheckoutUnderLoad(): void
{
$result = $this->runVoltTest(new VoltCheckout, [
'virtual_users' => 50,
]);

$this->assertVTSuccessful($result, 99);
$this->assertVTP95ResponseTime($result, 200);
$this->assertVTP99ResponseTime($result, 500);
$this->assertVTErrorRate($result, 1);
}
}
vendor/bin/phpunit --testsuite Performance

This catches performance regressions in CI before they reach production.

Data-Driven Load Testing with CSV

Using a single test account for all virtual users creates unrealistic session contention. CSV data sources solve this:

users.csv
email,password,name
alice@example.com,secret123,Alice
bob@example.com,secret123,Bob
carol@example.com,secret123,Carol
dave@example.com,secret123,Dave

In plain PHP, configure the data source on your scenario:

$scenario->setDataSourceConfiguration(
new DataSourceConfiguration(__DIR__ . '/users.csv', 'unique', true)
);

In Laravel, use the shorthand:

$scenario = $manager->scenario('User Flow')
->dataSource('users.csv', 'unique');

The iteration mode controls how rows are assigned to virtual users:

ModeBehavior
uniqueEach VU gets a different row. VUs > rows causes an error.
sequentialVUs cycle through rows in order, wrapping around.
randomEach VU gets a random row on each iteration.

Use unique for load tests where session isolation matters (login flows), and sequential or random for read-only endpoints where overlap is fine.

Reading Your Load Test Results

VoltTest output looks like this:

Performance Report: Checkout Flow Load Test
----------------------------------------------------------------------
Total Requests: 6000
Success Rate: 99.87%
Requests/Sec (RPS): 198.42
Avg Latency: 94.31ms
P95 Latency: 182.65ms
P99 Latency: 341.20ms
----------------------------------------------------------------------

What each metric tells you:

  • Success Rate — below ~99% means the app is dropping or erroring requests under this load level. Investigate error logs.
  • Requests/Sec (RPS) — your real-world throughput. If your expected peak is 200 RPS and the test shows 198, you're running at the limit with no headroom.
  • Avg Latency — useful as a baseline, but it masks outliers. An average of 94ms is meaningless if 1% of users wait 2 seconds.
  • P95 Latency — the response time 95% of requests complete within. This is your primary metric for user experience.
  • P99 Latency — the tail. If P99 is dramatically higher than P95, you have intermittent bottlenecks — likely database lock contention, garbage collection, or external service timeouts.

When load testing, run the same scenario at increasing VU counts (50, 100, 200, 500) and watch how P95 and RPS change. The point where P95 starts climbing sharply is your capacity limit.

Common PHP Load Testing Mistakes

A few mistakes make load test results misleading or useless:

  • Testing on localhost — your laptop is not production. Network latency, CPU, memory, and database all differ. Test against a staging environment that mirrors production hardware.
  • No ramp-up — slamming the app with all users at once tests a thundering herd, not real traffic. Use setRampUp() to add users gradually.
  • Single test user — every VU sharing one login causes session lock contention, which inflates latency. Use CSV data sources with unique users.
  • Only testing the homepage — the homepage is often cached and fast. Test the slow paths: search, checkout, report generation, admin dashboards.
  • Running from the same machine as the app — the load generator and the app compete for CPU and memory, distorting results. Run the test from a separate machine.
  • Watching averages instead of P95/P99 — an average of 80ms with P99 of 3 seconds means 1 in 100 users is having a terrible experience. Always read the tail.

Scaling Up with VoltTest Cloud

Local testing is limited by your machine's resources — a laptop can drive a few hundred to a few thousand virtual users depending on the scenario complexity. For production-scale validation (10,000+ concurrent users, multiple regions), VoltTest Cloud runs your tests on managed infrastructure.

The test code stays the same — just add the --cloud flag:

php artisan volttest:run CheckoutLoadTest --users=5000 --duration=5m --cloud

Or in plain PHP, enable cloud mode with your API key:

$test->cloud('vt_YOUR_API_KEY');
Closed Beta

VoltTest Cloud is now in closed beta. Sign up to join the waitlist — we review and approve access in waves.

CLOSED BETA

Join Early Access

Sign up to join the waitlist. We review and approve access in waves.

VoltTest is currently in closed beta. After signing up, your account will be reviewed and you'll be notified once approved.
Join Waitlist →Already have an account? Sign in

Conclusion

Load testing PHP applications doesn't require learning a new language or spinning up a complex infrastructure. With VoltTest, you write tests in PHP, install via Composer, and run them locally or at cloud scale. Start with a single endpoint, graduate to multi-step user flows, wire the tests into PHPUnit and CI, and monitor P95/P99 as load climbs. The goal isn't to generate impressive numbers — it's to know, with confidence, that your app handles the traffic you expect.

Learn More


Star the repository on GitHub: volt-test/php-sdk

💬 Follow updates on X: @VoltTest