# VoltTest — Complete Reference > Performance, load, and stress testing platform for PHP and Laravel developers. Powered by a Go engine scaling to 10M+ virtual users. ## Product Overview VoltTest is a cloud-based performance testing platform purpose-built for PHP and Laravel developers. It combines a high-performance Go engine with a native PHP SDK, so developers write tests in their own language — no JavaScript, no Java GUIs, no YAML configs. The platform runs in two modes: - **Headless mode**: Execute tests locally from the terminal (`php test.php` or `php artisan volttest:run`) - **Cloud mode**: Add `--cloud` to distribute load across dedicated cloud instances in 24 AWS regions, scaling to over 10 million concurrent virtual users ## Engine Performance - Go engine delivers 275 requests per second per virtual user - Each virtual user runs as a lightweight goroutine - Under 50 MB memory per 1,000 virtual users - Dedicated cloud instances per test — no shared infrastructure or noisy neighbors - Instances spin up automatically, generate load, and tear down on completion ## Features ### PHP SDK - Install via Composer: `composer require volt-test/php-sdk --dev` - Fluent PHP API for defining test scenarios - Multi-step scenario flows with weighted steps - Data providers for dynamic/parameterized payloads - Response extractors to chain data between steps - Configurable think times between actions - Response validation on status codes via validateStatus(); JSON and header values are read with the extractors rather than asserted per step ### Laravel Package - Install via Composer: `composer require volt-test/laravel-performance-testing --dev` - Artisan commands: `volttest:make` (generate test stubs), `volttest:run` (execute tests) - Automatic route discovery from your Laravel application - Session cookies handled automatically, one store per virtual user; CSRF token extraction is one call — `extractCsrfToken()` on the step that fetched the form - Supports both local and cloud execution ### Real-time Metrics Dashboard - Live updating during test execution - 30-second granularity, 1-second when zoomed to 5-minute window - Throughput (requests per second) - Response time percentiles: P50, P90, P95, P99 - Request success and error rates - Active virtual user count and ramp-up chart - Filter by scenario, step, or region ### Test Reporting - P50, P90, P95, P99 latency percentiles per run - Automatic error classification by type (timeout, connection reset, HTTP errors) - Request-level sampling for debugging individual requests - Run-to-run comparison with instant delta calculations - Per-scenario and per-step breakdown - Smart insights: auto-detect error spikes, P99 outliers, threshold breaches ### Distributed Cloud Testing - 24 AWS regions available; load split across several per run by weight - Per-region latency, throughput, and error rate metrics - Live geographic distribution map - Dedicated instances per test run - Automatic provisioning and teardown ## Pricing VoltTest uses a VU-hour billing model. 1 VU-hour = 1 virtual user running for 1 hour. ### Free Plan — $0/month - 500 concurrent virtual users - 5,000 VU-hours per month - Up to 10-minute test durations - 7-day data retention - No credit card required ### Solo Plan — $29/month ($23/month annual) - 5,000 concurrent virtual users - 50,000 VU-hours per month - Up to 30-minute test durations - 30-day data retention - Hard cap on VU-hours ### Pro Plan — $49/month ($39/month annual) - 25,000 concurrent virtual users - 250,000 VU-hours per month - Up to 60-minute test durations - 90-day data retention - Overage at $0.00065/VU-hour ### Business Plan — $149/month ($119/month annual) - 100,000 concurrent virtual users - 1,000,000 VU-hours per month - Up to 120-minute test durations - 365-day data retention - Overage at $0.00060/VU-hour ## Solutions ### PHP API Load Testing (https://volt-test.com/solutions/php-api-testing) - Load testing for REST and JSON APIs written in PHP — any framework, or none - Steps support GET, POST, PUT, PATCH, DELETE, HEAD and OPTIONS with arbitrary headers and a raw request body - A Content-Type header is required on any step that sends a body; step URLs are absolute - Values can be extracted from a JSON path, a response header, a cookie, a regex match or an HTML selector, then interpolated into a later step's URL, body or headers with ${variable} - Each virtual user runs the scenario independently with its own variables and, with automatic cookie handling enabled, its own cookie store - CSV data sources hand rows to virtual users sequentially, randomly, or uniquely (uniqueness preserved across nodes) - Load is either constant (virtual users + duration + optional ramp-up) or a staged profile; the two are mutually exclusive - Results report p95/p99, median, average, min/max response time, requests per second, and success rate - No GraphQL-specific support; the only per-step assertion in the PHP SDK is status-code validation ### Laravel Load Testing (https://volt-test.com/solutions/laravel-load-testing) - Composer package `volt-test/laravel-performance-testing`; requires Laravel 11, 12 or 13 on PHP 8.2+ and the pcntl extension (Unix only — not native Windows) - Exactly two Artisan commands: `volttest:make` scaffolds a test class, `volttest:run` executes it - `volttest:make --routes` reads the application's route table and writes one step per route, turning `{id}` segments into `${id}` variables; narrow with `--filter`, `--method`, `--auth` or `--select`. Route discovery does not exclude package routes, so Telescope/Horizon/debugbar appear unless filtered - Tests are plain classes in `app/VoltTests` implementing `VoltTest\Laravel\Contracts\VoltTestCase` with a single `define(VoltTestManager $manager): void` method - Session and cookie handling is automatic — every scenario enables it, so each virtual user holds its own Laravel session. CSRF is one line: `extractCsrfToken()` on the step that fetched the form, then `${csrf_token}` in the POST that follows - Steps take app-relative URLs (prefixed with the configured base URL) and accept PHP arrays as bodies, JSON- or form-encoded from the headers - `volttest:run` takes a class name, not a file path; `--cloud` runs the identical class on managed infrastructure; `--stage` and `--region` shape the load profile - Build gating is PHPUnit-only: extend `PerformanceTestCase` and assert with `assertVTP95ResponseTime`, `assertVTErrorRate`, `assertVTMinimumRPS`, `assertVTSuccessful`. The `volttest:run` command reports numbers but always exits successfully - Local runs print a console summary and write a JSON report to `storage/volttest/reports`; cloud runs report to the dashboard instead ### E-commerce Load Testing (https://volt-test.com/solutions/ecommerce-load-testing) - Rehearsing a sale spike against a storefront. Works with any storefront reachable over HTTP — WooCommerce, Magento, Shopware, PrestaShop, a custom Laravel build. No plugin required and none provided - Load shape is expressed as stages. **Each stage ramps to its target from wherever the previous stage ended** — a plateau is a stage whose target repeats the one before it. There is no hold(), spike() or soak() preset - Virtual user count is re-evaluated once per second; ramp-down retires the newest users first; a user added mid-ramp starts its scenario from step 1 - Weighted scenarios model the funnel: e.g. 70% browse-only, 30% browse-and-buy. Per-virtual-user cookie handling gives each shopper its own cart and session - CSV data sources in unique mode give each scenario run its own customer row. Rows are consumed per iteration, not per virtual user, and the file **wraps silently and reuses rows** once exhausted — size it above peak virtual users - **A request is scored successful when its status is 200–499.** Only 5xx and transport failures (timeout, reset, refused) count as failures. A 429 from a rate limiter, a 403 from a WAF, a 404 on a sold-out product and a 422 from a rejected checkout all count as successes unless the step carries an explicit status assertion. Attach validateStatus() to every step that matters - Does NOT detect overselling, double-charging or any cross-request consistency problem — responses are checked individually and no state is shared across virtual users - No payment-provider integration of any kind. The checkout call is an ordinary HTTP request; point it at a sandbox or test mode, never a live gateway - No JavaScript execution, so a headless or single-page storefront is exercised at its API layer only — no bundles, no client-side timings, no LCP/INP - Only test systems you own or are authorised to test (see https://volt-test.com/terms). All traffic carries `User-Agent: Volt-Test/` ## Comparisons ### VoltTest vs k6 - k6 uses JavaScript; VoltTest uses PHP — write tests in your application language - VoltTest offers a managed cloud; k6 Cloud is a separate paid product from Grafana Labs - VoltTest has native Laravel integration (Artisan commands, route discovery, per-virtual-user session handling, one-call CSRF token extraction); k6 has no framework integration - k6 has a larger ecosystem and extension library - k6 is open source; VoltTest is commercial with a free tier ### VoltTest vs JMeter - JMeter uses a Java GUI; VoltTest uses code-as-tests in PHP - VoltTest is cloud-native with managed infrastructure; JMeter requires self-hosted load generators - VoltTest's Go engine uses ~50 MB per 1,000 VUs; JMeter's Java runtime uses significantly more memory - JMeter has been around since 1998 with extensive community plugins - VoltTest provides instant cloud provisioning; JMeter requires manual infrastructure setup ## Quick Start ### PHP SDK ```php setVirtualUsers(100) ->setDuration('5m') ->setRampUp('30s'); $scenario = $test->scenario('Read users'); $scenario->step('Get Users') ->get('https://api.example.com/users') ->header('Accept', 'application/json') ->validateStatus('ok', 200); $result = $test->run(true); echo $result->getP95ResponseTime(); ``` Installed with: composer require volt-test/php-sdk Notes on the API: - The class is VoltTest\VoltTest; there is no TestManager class - The per-step status assertion is validateStatus(string $name, int $expected); there is no expectStatus() - Extraction methods are extractFromJson(), extractFromHeader(), extractFromCookie(), extractFromRegex() and extractFromHtml(); there is no extractJson() - Step URLs are absolute, and a Content-Type header is required on any step that sends a body - setVirtualUsers()/setDuration()/setRampUp() describe constant load; stage(duration, target) describes a staged profile, and the two are mutually exclusive ### Laravel ```bash composer require volt-test/laravel-performance-testing --dev php artisan volttest:make ApiTest php artisan volttest:run ApiTest --users=50 --duration=1m php artisan volttest:run ApiTest --cloud ``` The volttest:run argument is the test class name, not a file path. It also accepts a URL with --url for a direct one-off load test. ## About VoltTest VoltTest was founded by Islam A-Elwafa, a software engineer, after a Laravel API that passed all unit, feature, and browser tests failed in production under 200 concurrent users. The company first hired an external firm to run load tests, but the results could not be rerun in-house and the tooling lived outside PHP, so every new engineer had to learn a separate language and toolchain before writing a single test. He built VoltTest so PHP and Laravel teams could write load tests in PHP itself, in their own repository, with session cookies handled automatically and CSRF tokens and auth flows a single call away. Founder: Islam A-Elwafa (GitHub: https://github.com/elwafa, X: https://x.com/elwafa90, Email: islam@volt-test.com) Mission: Make load testing something every developer can write and understand, without depending on a specialist team or an external firm, with reports readable by everyone from a junior developer to the CTO. Vision: Support load testing in more languages beyond PHP over time, so teams on any stack can write load tests in the language they ship. ## Links - Website: https://volt-test.com - Features: https://volt-test.com/features - Pricing: https://volt-test.com/pricing - About (founder story): https://volt-test.com/about - Documentation: https://docs.volt-test.com - Laravel Docs: https://docs.volt-test.com/docs/laravel/laravel-installation - Solutions: https://volt-test.com/solutions - PHP API Load Testing: https://volt-test.com/solutions/php-api-testing - Laravel Load Testing: https://volt-test.com/solutions/laravel-load-testing - E-commerce Load Testing: https://volt-test.com/solutions/ecommerce-load-testing - Compare vs k6: https://volt-test.com/compare/volttest-vs-k6 - Compare vs JMeter: https://volt-test.com/compare/volttest-vs-jmeter - Compare vs LoadForge: https://volt-test.com/compare/volttest-vs-loadforge - GitHub: https://github.com/volt-test - Discord: https://discord.gg/BvQD6bptaD - X/Twitter: https://x.com/vt_developers - LinkedIn: https://www.linkedin.com/company/volt-test - Support: support@volt-test.com ## Glossary Standalone definition pages at https://volt-test.com/glossary — 20 load testing terms. ### Assertion URL: https://volt-test.com/glossary/assertion Also known as: Validation, Check An assertion is a check applied to a response during a load test — on status, body, or headers — that decides whether a request genuinely succeeded. A load test without assertions measures how fast a server can return bytes, not whether those bytes are right. This fails in a specific and expensive way: broken responses are usually faster than working ones, because the work never happened. ### Breakpoint Test URL: https://volt-test.com/glossary/breakpoint-test Also known as: Capacity Test, Breakpoint Testing A breakpoint test ramps load steadily upward until the system violates its targets or fails, in order to find the exact load at which capacity runs out. A breakpoint test exists to produce a single figure: the maximum load the system sustains while still meeting its targets. Everything about the design serves that output — a long, smooth, uninterrupted climb, with no plateaus or spikes to muddy where the curve turns. ### Concurrent Users URL: https://volt-test.com/glossary/concurrent-users Also known as: Concurrency, Simultaneous Users Concurrent users are the users active in a system at the same moment — in a load test, the virtual users holding an open session or an in-flight request. You set a virtual user count; you observe concurrency. The VU pool is only an upper bound. If half your VUs are sitting in think time at any given instant, actual request concurrency is roughly half the pool — and that gap widens the more realistic your think times are. ### Connection Pool URL: https://volt-test.com/glossary/connection-pool Also known as: Database Connection Pool, Pooling A connection pool is a fixed set of reusable open connections to a database or upstream service, shared between an application’s concurrent requests. Opening a database connection is expensive — a TCP handshake, authentication, session setup — and can easily cost more than the query that follows. A pool pays that cost once, keeps the connections open, and lends them out to requests that need one. ### Error Rate URL: https://volt-test.com/glossary/error-rate Also known as: Failure Rate, Error Percentage Error rate is the share of requests in a test that failed — connection errors, timeouts, unexpected status codes, or responses that failed an assertion. Error rate is the first number to read in any result, because it determines whether the rest of the numbers mean anything. A run reporting a 40 ms P95 and a 60% error rate is not a fast system; it is a system rejecting most of its traffic quickly. ### Latency URL: https://volt-test.com/glossary/latency Also known as: Network Latency, Time To First Byte Latency is the delay before a transfer begins — the time to get a request to the server and the first byte of the answer back, excluding transfer time. Latency is the wait before data starts flowing; response time is the whole round trip including the transfer. For a small JSON payload the two are nearly identical, which is why they get used interchangeably — and for a 5 MB download they are nothing alike. ### Load Profile URL: https://volt-test.com/glossary/load-profile Also known as: Load Pattern, Load Shape, Workload Model A load profile is the shape of load over time in a test — how many virtual users are active at each moment, from ramp-up through steady state to ramp-down. Two tests with an identical peak VU count can answer completely different questions depending on how they get there and how long they stay. The profile, not the peak, determines which failure modes a run can expose at all. ### P95 / P99 Percentiles URL: https://volt-test.com/glossary/percentiles-p95-p99 Also known as: P95, P99, 95th Percentile, 99th Percentile, Tail Latency A P95 of 800 ms means 95% of requests finished within 800 ms. P99 says the same at 99% — both describe the slow tail that an average conceals. Take 100 requests: 95 finish in 100 ms and 5 take 4 seconds. The mean is 295 ms, which sounds fine and describes not one single request in the set. The P95 is 4 seconds, which describes exactly what the unlucky users got. ### Ramp-Up URL: https://volt-test.com/glossary/ramp-up Also known as: Ramp Up, Ramp-Up Period Ramp-up is the period over which a test climbs from zero to its target load, adding virtual users gradually instead of starting all of them at once. Starting every virtual user in the same instant tests something no production system experiences. The first seconds of such a run measure cold caches, unwarmed JIT compilation, an empty connection pool, and autoscalers that have not reacted yet — all at once, and all attributed to your application. ### Requests Per Second (RPS) URL: https://volt-test.com/glossary/requests-per-second Also known as: RPS, QPS, Queries Per Second Requests per second is the number of HTTP requests a system handles each second — the most common concrete unit for expressing load test throughput. RPS counts individual HTTP requests. Transactions per second counts completed business operations, and one transaction usually spans several requests — a checkout might be six. Throughput is the general term for both, plus rates measured in bytes or messages. ### Response Time URL: https://volt-test.com/glossary/response-time Also known as: Round-Trip Time, Request Duration Response time is the total elapsed time from sending a request to receiving the complete response — everything the client waits for, network included. Response time is measured from the client’s point of view, which means it contains several things that are not your application code. When a number looks bad, breaking it into components usually shows where the time actually went. ### Saturation Point URL: https://volt-test.com/glossary/saturation-point Also known as: Knee Point, Saturation The saturation point is the load at which throughput stops rising even as more users arrive — the moment a resource runs out and queueing takes over. Plot achieved throughput and response time against active virtual users on the same time axis. Before saturation, throughput climbs in step with users and response time stays roughly flat. At saturation the two lines split: throughput flattens, and response time starts climbing. ### Scenario URL: https://volt-test.com/glossary/scenario Also known as: Test Scenario, User Journey A scenario is one complete user journey in a load test — an ordered sequence of steps that a virtual user executes from beginning to end. A scenario is more than a list of URLs, because the steps are connected. Step one logs in and the response carries a token; step two needs that token in a header; step three needs an order ID that only exists because step two succeeded. The scenario is what carries that state forward. ### Soak Test URL: https://volt-test.com/glossary/soak-test Also known as: Endurance Test, Longevity Test, Soak Testing A soak test holds a moderate, realistic load for hours or days to surface problems that only appear over time, such as memory leaks and pool exhaustion. Some defects are invisible at any load level and obvious at any duration. A handler that leaks 2 KB per request looks perfect in a ten-minute run and exhausts memory overnight. A log file that grows unrotated fills a disk on day three. A connection pool that leaks one connection in ten thousand requests takes hours to drain. ### Spike Test URL: https://volt-test.com/glossary/spike-test Also known as: Spike Testing, Surge Test A spike test applies a sudden, extreme jump in load and then removes it, to see whether a system absorbs the surge, sheds it cleanly, or falls over. Both push past normal load, but they differ in the axis they attack. A stress test raises the level of load until something gives. A spike test raises the rate of change — the peak may be entirely within capacity, yet arriving in five seconds instead of five minutes is what breaks things. ### Stress Test URL: https://volt-test.com/glossary/stress-test Also known as: Stress Testing A stress test pushes a system beyond its expected peak load to find where it degrades, what breaks first, and whether it recovers once the load drops. A load test asks whether the system meets its targets at expected load. A stress test asks what happens when it doesn’t. The difference is intent: a load test is a pass-or-fail check against a known number, while a stress test is an exploration of the region past that number, where the answer is not known in advance. ### Think Time URL: https://volt-test.com/glossary/think-time Also known as: Wait Time, Pacing Think time is the pause a virtual user takes between steps to imitate a real person reading, typing, or deciding before making the next request. A virtual user with no think time sends its next request the microsecond the previous response lands. That is not a user; that is a benchmark loop. It produces request rates no real population generates and a traffic pattern with none of the natural spacing that lets caches, pools, and queues recover between hits. ### Thread Group URL: https://volt-test.com/glossary/thread-group Also known as: Thread Group JMeter A thread group is JMeter’s unit of load: a named pool of threads that each run the same request sequence, with its own user count, ramp-up, and loop count. Thread group is Apache JMeter vocabulary, and it is the most widely recognised name for the concept because JMeter dominated load testing for well over a decade. In JMeter a test plan contains one or more thread groups, and every thread inside a group simulates one user running the samplers beneath it. ### Throughput URL: https://volt-test.com/glossary/throughput Also known as: Transaction Rate Throughput is the rate of work a system completes over time — usually requests, transactions, or bytes per second, measured at the completing end. You configure virtual users and think time; the system decides the throughput. This is the most common misreading of a load test result — treating a throughput figure as the load that was applied rather than the work that got done. ### Virtual Users (VUs) URL: https://volt-test.com/glossary/virtual-users Also known as: VU, VUs, Virtual User A virtual user is a simulated client that runs a test scenario end to end, independently and in parallel with every other virtual user in a test. A load generator creates a pool of virtual users and hands each one a copy of the scenario — the ordered list of requests that represents a single user journey. Each VU walks its own copy of that script: it opens its own connection, keeps its own cookies and extracted variables, and only moves to the next step once the previous one has returned. ## Blog ### Magento 2 Load Testing Tools: 5 Options Compared URL: https://volt-test.com/blog/magento-load-testing-tools Published: 2026-09-14 Keywords: magento 2 load testing, load testing tools for magento, magento load testing tools, magento load testing, load test magento 2, adobe commerce load testing, magento performance testing, magento 2 checkout performance, magento concurrent users, magento jmeter performance toolkit, k6 magento, volttest php sdk # Magento 2 Load Testing Tools Compared: JMeter, k6, Gatling, Siege and VoltTest Magento 2 load testing has been a JMeter job for a decade, and that was fair: Adobe shipped a JMeter plan in the core repository and every agency runbook pointed at it. That plan no longer ships with the codebase. The store you are testing probably runs a front-end architecture the original JMeter plan predates, such as Hyvä or PWA Studio. And the people asked to run the test are PHP developers, not performance engineers. This post compares the five load testing tools for Magento that an Adobe Commerce or open-source Magento team hears about when it asks "what should we load test with": Apache JMeter with the Performance Toolkit, k6 with the community Magento toolkit, Gatling, Siege and VoltTest. It ends with a guest checkout scenario in PHP, because checkout is often where Magento's uncached, write-heavy workload exposes capacity limits first, and no tool gives it to you for free. One disclosure: I build VoltTest, and I ran the [Laravel](/blog/laravel-load-testing) and [Symfony](/blog/symfony-load-testing) benchmarks on this blog the same way. Where it falls short for Magento I say so, and there are no benchmark numbers on this page; the measured results are coming in a follow-up. TL;DR: Any of the five will hammer a category page through Varnish. Four of the five can model a checkout flow. JMeter is the only one here with an existing Magento-specific checkout plan; with k6, Gatling and VoltTest, you define the flow yourself. JMeter is the incumbent: Adobe's own plan covers browsing, checkout, admin, REST and GraphQL, but it stopped shipping with the codebase after 2.4.5 and it is a QA tool. k6 is the modern default if your team writes JavaScript; the community Magento toolkit is a single-URL runner, so the checkout is yours to write. Gatling has strong reporting and is less common in PHP shops. Siege cannot hold a cart id, so it is a Varnish smoke test. VoltTest lets you write the whole thing in PHP next to the store, at the cost of driving the REST layer rather than a browser. ![Magento 2 load testing tools compared: JMeter, k6, Gatling, Siege and VoltTest against a Magento 2 storefront](/img/magento-load-testing-tools-og.png) ## Why Magento 2 Load Testing Is Different A tools comparison only makes sense against the thing being tested, and Magento performance testing has three properties that disqualify most generic advice. **Full page cache hides the real capacity.** With Varnish in front, an anonymous visit to the home page, a category or a product that hits full page cache never touches PHP. A load test that only browses cached catalogue pages is therefore closer to a Varnish benchmark than a Magento one. The moment a shopper adds to cart, logs in or reaches checkout, every request goes to php-fpm, MySQL and Redis. You can tell the two apart from outside: Varnish sets an `Age` header above zero on a hit, and in developer mode Magento adds `X-Magento-Cache-Debug: HIT` or `MISS`. **Checkout is write-heavy and uncached.** Creating a cart inserts into `quote`, and every item change recalculates totals. Estimating shipping runs the carrier collectors. Placing the order writes `sales_order`, its items and addresses, and, on a store with the Inventory Management modules installed, an `inventory_reservation` row per product, all in one transaction. None of it is cacheable. A test that never reaches `payment-information` has skipped one of the most expensive parts of the purchase flow. **The first ceiling is often not raw web-server throughput.** PHP worker capacity, CPU, memory, MySQL, Redis, OpenSearch and third-party integrations can each become the limiting factor. Which one goes first depends on the store. Two are easy to miss. `pm.max_children` is often bounded by RAM before the cores are busy, and once workers run out requests queue in nginx and time out at Varnish. Search and layered navigation go to OpenSearch, a separate service with its own ceiling. | Request | Typical backend path | FPC/Varnish | |---|---|---| | Home, category, product, CMS page (anonymous) | Varnish on hit; Magento/PHP on miss | Cacheable | | Same pages, logged-in customer | php-fpm for private blocks, MySQL, Redis | Public blocks cacheable | | Search, layered navigation | php-fpm, OpenSearch | Not cached | | Add to cart, cart page, mini-cart | php-fpm, MySQL, Redis sessions | Not cached | | Shipping estimate, shipping and payment information | php-fpm, MySQL, carrier and payment integrations | Not cached | | Place order | php-fpm, MySQL transaction, stock update or reservation, queue publish | Not cached | So the requirements for a Magento 2 load testing tool are concrete. It has to hold a cart id per [virtual user](/glossary/virtual-users) and reuse it, because Magento hands you a masked id you did not choose. It has to weight scenarios so checkout is a realistic fraction of traffic. And it has to run from more than one machine when the target is bigger than the box generating load. ## Magento 2 Load Testing Tools Compared Locust and Artillery can also script a Magento checkout flow; they are left out of this comparison only because they are less common choices in Magento-focused teams, not because they cannot do the job. | Tool | Test language | Magento checkout scripting | CI fit | Distributed runs | Best for | |---|---|---|---|---|---| | [JMeter](/compare/volttest-vs-jmeter) + Performance Toolkit | XML plan built in a Java GUI | Done for you in Adobe's plan, up to 2.4.5 | CLI mode works, artefacts are large | Yes, with JMeter servers you run | QA teams, Adobe-aligned agencies | | [k6](/compare/volttest-vs-k6) + community toolkit | JavaScript | Toolkit is a single URL; write checkout yourself | Excellent | Self-hosted k6 operator or Grafana Cloud k6 | Teams that already write JS | | Gatling | Java, Kotlin, Scala, JavaScript or TypeScript SDKs | Write it yourself; good correlation | Good | Gatling Enterprise | JVM or TypeScript shops, report-heavy reviews | | Siege | Command line, URL list | Not possible, no extraction | Trivial | No | Varnish warm-up and one-URL checks | | [VoltTest](https://docs.volt-test.com) | PHP | Write it yourself against REST or GraphQL; extractors and per-user cookies built in | PHP script, exit code, thresholds | Built in, multi-region cloud | PHP teams who want the test in the store repo | ### JMeter with the Performance Toolkit For most of Magento 2's life, "Magento load testing" meant one file: `setup/performance-toolkit/benchmark.jmx`. It shipped in the core repository up to and including 2.4.5, and it is a serious piece of work. The 2.4.5 plan is over seven megabytes of XML with nine thread-group pools: a frontend pool that browses, searches, adds simple and configurable products to cart and runs checkout through shipping, payment and place order, plus admin, customer-service, REST API, GraphQL and combined pools. It needs the JMeter JSON plugins, and its README tells you to clear active quotes in MySQL before every run. From 2.4.6 onward the directory holds only the fixture profiles and the `setup:perf:generate-fixtures` command, which is still the best way to get a realistic catalogue for any tool on this page: the small profile generates roughly 800 simple products, 30 categories and 200 customers. Adobe's [data generation docs](https://experienceleague.adobe.com/en/docs/commerce-operations/configuration-guide/cli/generate-data) cover the profiles; the JMeter plan you now pull from the [2.4.5 tag](https://github.com/magento/magento2/tree/2.4.5/setup/performance-toolkit) and adapt. That is the case for JMeter: the scenarios were written by the people who wrote the checkout, and an Adobe Commerce support engineer will recognise the output. The case against it is the tooling. The plan is XML you edit in a Java desktop GUI, it is too large to review in a pull request, and a seven-megabyte file nobody on the team wrote is the definition of a test that rots. It is a QA department's tool, and if that is who owns performance at your company it is the right one. The [VoltTest vs JMeter](/compare/volttest-vs-jmeter) page covers the developer-experience differences in detail. ### k6 with the community Magento toolkit k6 is the tool most developers reach for today: a single Go binary, tests in JavaScript, thresholds that fail the CI job. Each virtual user gets its own cookie jar and reading a value out of a JSON response is one line, so a Magento checkout is entirely scriptable in k6. Be clear about what the community toolkit gives you, though. The most visible "Magento k6 performance toolkit" on GitHub is a single script that hits one URL with a configurable number of users, appends a per-request query string to bust full page cache when asked, and reports p95. The cache-busting trick is worth stealing, because it is the simplest way to measure the store instead of Varnish. But there is no category walk, no add to cart and no checkout in it. The correlation work, from the masked cart id through the shipping and payment calls, is yours to write and maintain in JavaScript. That is the real cost for a PHP team: the test lives in a second language, and distributed runs typically mean operating the k6 Kubernetes infrastructure yourself or using Grafana Cloud k6. The [VoltTest vs k6](/compare/volttest-vs-k6) comparison covers where k6's broader protocol support wins, which it does for WebSocket or gRPC work that Magento does not need. ### Gatling Gatling has particularly strong built-in reporting, and its ecosystem has historically been less natural for PHP-heavy Magento teams. Scenarios are written with official SDKs for Java, Kotlin, Scala, JavaScript or TypeScript, so it is no longer a JVM-only tool. Correlation is first class, so a checkout is no harder than in k6, and the HTML report after every run can go in front of a client unedited. But few Magento teams already have a Gatling project, there is no Magento-specific starting point, and distributed runs need Gatling Enterprise. With a JVM or TypeScript team next door it is a fine choice; otherwise it is a second stack to learn for one job. ### Siege Siege is a C command-line tool that takes a URL or a file of URLs, a concurrency and a duration, and reports throughput and response time. It is the right tool for two Magento jobs: warming Varnish after a deploy, and checking that a category page still returns 200 under a few hundred concurrent hits. It is not a load testing tool for Magento 2 in the sense this article means, because it cannot read a response and reuse a value, so no cart id, no add to cart, no order. If a vendor shows you a Siege number for a Magento store, it is a Varnish number. ### VoltTest VoltTest is the tool I build, and its pitch to a Magento team is simple: the load test is a PHP file, it lives in the store's repository next to the code it protects, and it runs locally or in the cloud from several regions without changing a line. Steps are HTTP calls with headers and bodies. Extractors pull values from JSON, headers, cookies, HTML and regex into variables the next step interpolates as `${var}`. Each virtual user gets its own cookie jar when you ask for it, and scenarios are weighted with `setWeight()` so checkout can be 5% of traffic while browsing is 80%, the mix the [e-commerce load testing](/solutions/ecommerce-load-testing) page describes. Where VoltTest stops for Magento: VoltTest is an HTTP client. It does not execute JavaScript, so it will not run Luma's Knockout checkout, a Hyvä theme's Alpine.js or a PWA Studio React app. You script the calls those front ends make. For Luma, and for Hyvä Theme running the stock Luma checkout through its theme fallback, that is the `/rest/V1/guest-carts` and `/rest/V1/carts/mine` flow shown below. Hyvä Checkout is a separate product built on Magewire and does not use that REST checkout flow. The script in this post does not cover it; you would script Magewire's own requests instead. PWA Studio sends GraphQL mutations to `/graphql`, which are ordinary POSTs with a JSON body. That is the right layer for finding the capacity of php-fpm, MySQL and OpenSearch, and the wrong layer for Core Web Vitals, which are out of scope. The only per-step assertion is `validateStatus()`, so a checkout that returns 200 with an error in the body needs a regex extractor to catch it. Data sources are CSV files, and if your compliance process wants a JMeter artefact, VoltTest will not produce one. Within those limits it correlates the masked cart id, weights scenarios and runs distributed by default. The rest of this post shows the correlation part, because that is where every Magento checkout script, in any tool, either works or does not. ## A Guest Checkout Scenario in PHP Luma's checkout is a JavaScript front end over five REST calls, and the same five calls work from any HTTP client. Hyvä Theme with the Luma checkout fallback makes the same calls; Hyvä Checkout does not, as noted above. In order: create a guest cart, add an item, estimate shipping methods, set shipping information, set payment information. The last one places the order. Adobe's [order tutorial](https://developer.adobe.com/commerce/webapi/rest/tutorials/orders/) walks the same sequence with curl. The script below, the Magento 2.4.8 store it was validated against and the Docker stack that runs both are at [volt-test/magento-load-testing](https://github.com/volt-test/magento-load-testing), with the SDK in the store's own `composer.json`. Two things trip up every first attempt. First, `POST /rest/V1/guest-carts` does not return an object. It returns a bare JSON string, the masked quote id, and so does the customer token from `integration/customer/token`. The order id from `payment-information` is a bare scalar too, a quoted string on some installs and a plain number on others, which is why the regex below makes the quotes optional. A JSON path extractor expects an object at the root, so those responses are read with a regex. Second, the store needs a payment method and a shipping method with no third party behind them. The example deliberately uses Magento's offline Check / Money Order method, `checkmo`, together with `flatrate`, both built in and both enabled on a default install, so the benchmark exercises Magento's own checkout without adding an external payment gateway as another dependency, and another potential bottleneck, to the measurement. If your store has switched them off, turn them back on for the test environment. ```bash composer require volt-test/php-sdk ``` ```php title="loadtest/magento-guest-checkout.php" use VoltTest\DataSourceConfiguration; use VoltTest\VoltTest; $base = rtrim(getenv('TARGET_URL') ?: 'https://magento.test', '/'); $test = (new VoltTest('Magento guest checkout')) ->target($base) ->setVirtualUsers((int) (getenv('VUS') ?: 20)) ->setDuration(getenv('DURATION') ?: '2m') ->setRampUp('30s'); $address = '{"firstname":"Load","lastname":"Test","street":["1 Test St"],' . '"city":"Los Angeles","region":"CA","region_id":12,"region_code":"CA",' . '"country_id":"US","postcode":"90001","telephone":"5555555555","email":"${email}"}'; $checkout = $test->scenario('Guest checkout via REST') ->setWeight(5) ->setThinkTime('2s') ->autoHandleCookies() ->setDataSourceConfiguration( new DataSourceConfiguration(__DIR__.'/data/skus.csv', 'random', true) ); $checkout->step('Create guest cart') ->post("$base/rest/V1/guest-carts") ->header('Content-Type', 'application/json') ->validateStatus('cart created', 200) ->extractFromRegex('cart_id', '^"([A-Za-z0-9]+)"$'); $checkout->step('Add item') ->post("$base/rest/V1/guest-carts/\${cart_id}/items", '{"cartItem":{"sku":"${sku}","qty":1,"quote_id":"${cart_id}"}}') ->header('Content-Type', 'application/json') ->validateStatus('item added', 200); $checkout->step('Estimate shipping') ->post("$base/rest/V1/guest-carts/\${cart_id}/estimate-shipping-methods", '{"address":{"country_id":"US","region_id":12,"postcode":"90001"}}') ->header('Content-Type', 'application/json') ->validateStatus('shipping estimated', 200); $checkout->step('Set shipping information') ->post("$base/rest/V1/guest-carts/\${cart_id}/shipping-information", '{"addressInformation":{"shipping_address":'.$address.',"billing_address":'.$address.',' . '"shipping_carrier_code":"flatrate","shipping_method_code":"flatrate"}}') ->header('Content-Type', 'application/json') ->validateStatus('shipping set', 200); $checkout->step('Place order') ->post("$base/rest/V1/guest-carts/\${cart_id}/payment-information", '{"email":"${email}","paymentMethod":{"method":"checkmo"},"billing_address":'.$address.'}') ->header('Content-Type', 'application/json') ->validateStatus('order placed', 200) ->extractFromRegex('order_id', '^"?([0-9]+)"?$'); $test->run(true); ``` Repeated `checkmo` orders consume real fixture inventory. Every placed order takes stock, and once a SKU runs out the failures start: a cart that already holds the last unit gets 400 `Some of the products are out of stock.` from `payment-information`, and a fresh cart gets 400 `Product that you are trying to add is not available.` from `items`. The wording is the same with and without the Inventory Management modules. On a dashboard that looks exactly like a Magento capacity failure. Either generate fixtures with a large enough quantity or enable backorders for the benchmark, or reset the relevant order, quote and inventory state between runs. The Performance Toolkit README's advice to deactivate open quotes before each JMeter run is the same operational idea, but clearing quotes alone does not put stock back. The `skus.csv` file has `sku` and `email` columns: export the SKUs from `catalog_product_entity` after generating fixtures. `${email}` goes into the guest address and the final payment call because a guest order needs one; it comes from the CSV so that generated orders are easy to identify and trace in Magento Admin. A unique generated email is useful for benchmark traceability, but Magento does not require it to be unique to accept the guest address. The regex on the first step captures the id inside the quotes Magento returns, and from then on `${cart_id}` is interpolated into every later URL. In a PHP double-quoted string the dollar sign is escaped as `\${cart_id}` so PHP leaves it for the engine; the single-quoted JSON bodies need no escaping. The `$base` prefix is repeated on purpose: the SDK validates every step URL as an absolute URL and the engine sends it as given, so `target($base)` identifies the target for the run but does not prefix step paths. Every step asserts a 200 with `validateStatus()`. Magento answers 400 with a JSON `message` on validation failures and 404 when the masked id is wrong, so a broken correlation shows up as an error-rate spike on that step, not a silently green run. The `shipping-information` response is a real object, `{"payment_methods":[...],"totals":{...}}`, and it is where a JSON path extractor works if you need something from it; this example does not, because the payment method is fixed. To turn this into a store test, add a browse scenario at `setWeight(80)` that walks home, category, product and search, and a cart scenario at 15 that stops after `items`. A 200-user run then sends about ten shoppers through checkout at a time and reports [p95](/glossary/percentiles-p95-p99) and error rate per step. Customer checkout is the same sequence with a bearer token from `integration/customer/token` and `/rest/V1/carts/mine`, fed from a CSV of fixture customers in `unique` mode so no two virtual users share a session. ## Which Magento 2 Load Testing Tool Should You Use? ### Which load testing tool is best for Magento 2? Of all the Magento load testing tools, the best is the one that can script checkout and that the team will still run after the launch. That means JMeter with the Performance Toolkit if a QA group owns the test and wants Adobe's own scenarios, k6 if your team is comfortable in JavaScript and will run its own load generators, and VoltTest if you want the test in PHP with the infrastructure handled. - **A QA team owns it, or Adobe support is involved.** JMeter. Pull the 2.4.5 plan, point it at `small` profile fixtures, accept the GUI. - **Your developers write JavaScript and already run Grafana.** k6. Use the toolkit's cache-busting trick for browse, then write the five REST calls above in JavaScript. - **Your developers write PHP and the store is one repository.** VoltTest. The scenario above is the whole checkout, and it runs in CI with an exit code and in the cloud from several regions. - **You have a JVM or TypeScript team next door.** Gatling, mostly for the reports. - **You need to warm Varnish or check one page.** Siege, and do not call the result a load test. Whichever you choose, generate fixtures with the Performance Toolkit so the catalogue is realistic, script checkout through to `payment-information` rather than stopping at add to cart, and weight checkout at the fraction your analytics show. A test that is half checkout finds a [saturation point](/glossary/saturation-point) real traffic never reaches, and a test with no checkout misses the one that matters. ## What Comes Next This is the tools half. The measured half is a Magento 2.4 store with MySQL, OpenSearch, Redis and Varnish, loaded with the Performance Toolkit small profile and pushed from 50 to 800 concurrent users with the scenario above at 5% of traffic: warm versus cold Varnish on the same box, anonymous versus logged-in shoppers, and the user count where checkout starts returning errors. There are no numbers here because those runs are not finished, and I would rather publish them once than revise them. When the follow-up is live it will be linked from this page and from the [e-commerce load testing](/solutions/ecommerce-load-testing) hub. ## Resources - [volt-test/magento-load-testing](https://github.com/volt-test/magento-load-testing): the Magento 2.4.8 project, Docker stack and checkout script from this post, with the raw REST responses captured during validation - [Generate data for performance testing](https://experienceleague.adobe.com/en/docs/commerce-operations/configuration-guide/cli/generate-data): Adobe's fixture profiles and the `setup:perf:generate-fixtures` command - [Performance Toolkit at Magento 2.4.5](https://github.com/magento/magento2/tree/2.4.5/setup/performance-toolkit): the last release that includes `benchmark.jmx` - [Adobe Commerce REST order tutorial](https://developer.adobe.com/commerce/webapi/rest/tutorials/orders/): the guest cart, items, shipping and payment calls used above - [Configure and use Varnish](https://experienceleague.adobe.com/en/docs/commerce-operations/configuration-guide/cache/varnish/config-varnish): full page cache setup and how to tell a hit from a miss - [Apache JMeter](https://jmeter.apache.org/), [k6](https://k6.io/), [Gatling](https://gatling.io/), [Siege](https://github.com/JoeDog/siege) - [VoltTest documentation](https://docs.volt-test.com/docs/introduction), [steps and extractors](https://docs.volt-test.com/docs/Steps), [scenarios and data sources](https://docs.volt-test.com/docs/Scenarios) - Related: [PHP load testing](/blog/php-load-testing), [Laravel load testing](/blog/laravel-load-testing), [Symfony load testing](/blog/symfony-load-testing) VoltTest's PHP SDK is open source on [GitHub](https://github.com/volt-test/php-sdk). If this comparison saved you a week in a JMeter GUI, a star helps the next Magento developer find it. --- ### Symfony Load Testing: FrankenPHP vs PHP-FPM URL: https://volt-test.com/blog/symfony-load-testing Published: 2026-09-11 Keywords: symfony load testing, load test symfony, how to load test symfony, symfony performance testing, symfony stress testing, symfony load testing tools, symfony concurrent users, frankenphp worker mode, frankenphp vs php-fpm, symfony csrf load test, lexik jwt load test, doctrine under load, volttest php sdk # How to Load Test Symfony Applications: FrankenPHP vs PHP-FPM With Real Results Most Symfony load testing advice comes in two flavours. The first is a checklist: tune Doctrine, cache more, watch PHP-FPM. The second is a benchmark of a controller that returns `{"hello":"world"}`, which is how FrankenPHP got its famous 3x headline. Neither tells you what your app does when 2,000 people log in, add the same product to their cart and press checkout at the same time. So I built a Symfony 7.4 shop the way I would ship one: form login with CSRF, a JWT API, Doctrine on PostgreSQL, Redis sessions and a real checkout transaction. I load tested it in pure PHP with [VoltTest](https://docs.volt-test.com). Then I ran the identical test against the identical app on nginx and php-fpm, and again with a cheaper password hash. Every number in this guide changes exactly one variable at a time. This is the Symfony load testing companion to my [Laravel load testing guide](/blog/laravel-load-testing) and part of the wider [PHP load testing](/blog/php-load-testing) series. It covers what breaks in Symfony under load, how the tools compare, how to write the test in PHP without leaving your stack, what the numbers were, and one experiment that proves a checkout cannot oversell. TL;DR: Symfony load testing catches what PHPUnit cannot: login throughput capped by password hashing, sessions serialising on file locks, and checkouts fighting over one product row. On one 2-core Fly.io machine this shop saturated at about 100 concurrent users and 20 requests per second, and every user beyond that only added queue time, because a bcrypt cost-13 login costs 479 ms of CPU on that box. FrankenPHP worker mode served 14% more requests than php-fpm over the same ramp and 1.5x the peak throughput, then hit the same wall in the same place. Dropping the hash cost to 10 moved the wall to 400 users and 118 requests per second with zero errors, five times the traffic. And 500 buyers racing for 500 units bought exactly 500. The test scripts are PHP, the app and scripts are on [GitHub](https://github.com/volt-test/symfony-load-testing-example), and every number below is reproducible. ![Symfony load testing with VoltTest: FrankenPHP worker mode versus PHP-FPM on a real shop with CSRF, JWT and checkout](/img/symfony-load-testing-og.png) ## What Breaks in Symfony Under Load Symfony is unusually well behaved under load compared with most PHP frameworks. The compiled container, opcache preload and a warm cache mean the framework itself is rarely the bottleneck. Symfony performance problems under concurrency almost always live in the five places below, and a Symfony load test is the only kind of test that reaches them. ### Doctrine and the hot row Everyone knows about N+1 queries, and the profiler catches them at one user. The failure that only shows up under load is contention. A checkout that decrements stock touches one row per product, and when 500 buyers want the same product, PostgreSQL serialises them on that row. Each transaction holds the lock for as long as it runs. The time you spend inside it, hydrating entities and flushing the order, becomes queueing time for everyone behind you. Later in this guide there is a [flash sale experiment](#the-flash-sale-proving-checkout-never-oversells) that measures exactly this. ### Sessions: file locking versus Redis Symfony's default session handler writes to the filesystem, and PHP locks the session file for the whole request. One user opening three tabs is fine. Two hundred users behind a load balancer with a shared volume is a lock convoy. Moving sessions to Redis with the built-in `RedisSessionHandler` removes the lock and lets the app run on more than one machine, which is why the test app uses it from the start. Removing the lock is a trade-off, not a free win: Symfony's handler [does no session locking at all](https://symfony.com/doc/current/session.html#store-sessions-in-a-key-value-database-redis), so two concurrent requests that both write to the same session, such as parallel AJAX calls, can overwrite each other, and the usual symptom is a spurious "Invalid CSRF token". If your app writes to the session from concurrent requests, use the phpredis extension's native handler with `redis.session.locking_enabled = 1` instead, and accept that requests from the same user serialise again. ### Password hashing is CPU, not I/O This is the one nobody budgets for. Symfony's `auto` password hasher currently means bcrypt, and in this app it resolved to cost 13 (every stored hash starts with `$2y$13$`). Argon2id only comes into play if you configure the `sodium` hasher explicitly, and it has a cost of its own. On my laptop one login costs about **365 ms** of pure CPU and a product page about **2 ms**. On the 2-core Fly machine used for the cloud runs a bcrypt verify costs **479 ms**. That caps the box at roughly 4 logins per second no matter how fast the rest of the stack is. If your load test logs in on every iteration, and most do, login is your throughput ceiling. The numbers section shows what happens when the cost drops to 10. ### CSRF and the stateless trap Recent Symfony recipes enable *stateless* CSRF tokens for login, logout and form submits. The hidden field holds a placeholder that a Stimulus controller fills client-side, and without JavaScript the check silently falls back to `Origin` and `Referer` headers. That is fine for browsers and invisible to a load test, which then passes without ever exercising your session layer. The test app switches back to session-backed tokens, which is what most deployed Symfony apps still run. So the test has to fetch the form, extract the token and hold the session cookie like a real client. ### php-fpm worker saturation versus worker mode Under php-fpm, every request boots the kernel, and when requests outnumber `pm.max_children` they queue in nginx. FrankenPHP worker mode boots Symfony once per worker thread and handles requests in a loop, which removes the bootstrap cost entirely. The published benchmarks put that at 3x. Whether you get 3x depends on how much of your request was bootstrap in the first place, which is the question this guide's comparison answers. ## Symfony Load Testing Tools Compared Any HTTP load generator can hit a Symfony app, so the Symfony load testing tools question is really about ergonomics. The differences that matter for Symfony are whether the tool handles CSRF and session cookies without a fight, whether your team has to learn another language to use it, and how far it scales. | Tool | Test language | CSRF and sessions | CI integration | Scale | Fit for Symfony teams | |---|---|---|---|---|---| | [VoltTest](https://docs.volt-test.com) | PHP | Cookie jar per virtual user, CSS-selector extractors for tokens | PHPUnit assertions, exit codes | Local runs, distributed multi-region runs in the cloud | Native | | [k6](/compare/volttest-vs-k6) | JavaScript | Automatic per-VU cookie jar; CSRF extraction and correlation written in JavaScript | Excellent | High, cloud is paid | Good if the team already writes JS | | [JMeter](/compare/volttest-vs-jmeter) | XML via GUI | Regex or CSS extractors, cookie manager | Possible, clunky | High with distributed setup | Heavy, but everywhere | | Gatling | Java, Kotlin, Scala, JavaScript or TypeScript DSL | Good | Good | High | Rare in PHP shops | | [LoadForge](/compare/volttest-vs-loadforge) | Python (Locust) | Manual | Yes | Cloud only | Another language and a hosted dependency | | `ab`, `wrk` | Command line | None | Trivial | Single machine | Smoke tests of one URL only | When to use each: `wrk` for a ten-second sanity check of a single endpoint; k6 or Gatling if the people writing the tests are not PHP developers; JMeter if compliance demands it; VoltTest if you want the test to live in the same repository, language and CI pipeline as the Symfony app it protects. The rest of this guide uses VoltTest, and the disclosure at the top of the page applies. I built it. ## The Test App: a Production-Mode Symfony Shop Load testing a `hello world` measures the runtime. Load testing a real Symfony app measures your architecture. The target here is a small shop, deliberately configured the way a production Symfony app would be, not the way a dev container is. | Piece | Choice | Why it matters under load | |---|---|---| | Framework | Symfony 7.4 LTS on PHP 8.4 | The current LTS. FrankenPHP worker mode is natively supported in Symfony 7.4 and later, so no runtime bridge package is needed (the repo still lists `runtime/frankenphp-symfony` in composer.json, but with no `APP_RUNTIME` set it is inert and the native runtime served every run) | | Runtime | FrankenPHP worker mode with opcache preload of the compiled container | Kernel boots once per worker, nothing is re-parsed | | Database | PostgreSQL through Doctrine ORM (16 in the local Compose stack, 18 on Fly for the cloud runs) | Stock is reserved with a conditional `UPDATE` inside the checkout transaction | | Sessions | Redis via `RedisSessionHandler` | No file locks, works on more than one machine; no session locking either, see above | | API auth | `json_login` plus LexikJWTAuthenticationBundle on a stateless firewall | Bearer tokens, no session on `/api/*` | | Web auth | `form_login` with session CSRF, Form component CSRF on add-to-cart | The realistic path a browser takes | Two design rules make it load testable. Every failure returns a real status code, because a load test asserts on status: invalid form 422, bad CSRF on checkout 403, empty cart or sold out 409, bad JWT 401. Symfony re-renders an invalid form with 200 by default, which would make a broken test look green. And the catalog has a `flash-sale` product with exactly 500 units so that oversell can be measured rather than assumed. The app exposes the same shop twice: a JSON API under `/api/v1` (login, products, cart, checkout) and server-rendered pages (`/login`, `/products/{id}`, `/cart`, `/checkout`). The whole thing, load test included, is at [volt-test/symfony-load-testing-example](https://github.com/volt-test/symfony-load-testing-example). ## Step-by-Step: How to Load Test Symfony in PHP The whole Symfony load test is one PHP file. It defines four scenarios that share a pool of [virtual users](/glossary/virtual-users), each fed from its own CSV of accounts, and it runs either on your laptop or in VoltTest Cloud without changing a line. ### Install the SDK ```bash mkdir loadtest && cd loadtest composer require volt-test/php-sdk ``` The SDK downloads the load-generating engine, a single Go binary, on first run. Nothing else to install, no JVM, no Node. ### The API scenario: JWT login, products, cart ```php title="loadtest/symfony-shop-test.php" use VoltTest\DataSourceConfiguration; use VoltTest\VoltTest; $base = rtrim(getenv('TARGET_URL') ?: 'http://localhost:8088', '/'); $test = (new VoltTest('Symfony Shop')) ->target($base) ->setVirtualUsers((int) (getenv('VUS') ?: 200)) ->setDuration(getenv('DURATION') ?: '2m') ->setRampUp('30s'); $users = fn (string $shard) => new DataSourceConfiguration(__DIR__."/data/users-$shard.csv", 'unique', true); $browse = $test->scenario('API: browse and add to cart') ->setWeight(50) ->setThinkTime('1s') ->setDataSourceConfiguration($users('1')); $browse->step('Login (JWT)') ->post("$base/api/v1/login", '{"email":"${email}","password":"${password}"}') ->header('Content-Type', 'application/json') ->validateStatus('login ok', 200) ->extractFromJson('token', 'token'); $browse->step('Product detail') ->get("$base/api/v1/products/\${product_id}") ->validateStatus('product ok', 200); $browse->step('Add to cart') ->post("$base/api/v1/cart/items", '{"product_id":${product_id},"quantity":${quantity}}') ->header('Content-Type', 'application/json') ->header('Authorization', 'Bearer ${token}') ->validateStatus('cart item created', 201); ``` `extractFromJson('token', 'token')` reads Lexik's `{"token": "..."}` response into a variable, and `${token}` interpolates it into the `Authorization` header of every later step. The `${email}`, `${product_id}` and `${quantity}` variables come from the CSV row that virtual user was handed. ### The HTML scenario: CSRF, sessions and why it logs out This is the scenario that breaks most Symfony load tests on the first run, and it is the reason a Symfony load testing tool has to understand cookies. A browser gets the session cookie and the CSRF token for free. A load generator has to do what the browser does, in order. ```php title="loadtest/symfony-shop-test.php (HTML shopper)" $html = $test->scenario('HTML: login, add to cart, checkout') ->setWeight(30) ->setThinkTime('2s') ->autoHandleCookies() ->setDataSourceConfiguration($users('3')); $html->step('Login page') ->get("$base/login") ->validateStatus('login page ok', 200) ->extractFromHtml('login_csrf', 'input[name="_csrf_token"]', 'value'); $html->step('Submit login form') ->post("$base/login", '_username=${email}&_password=${password}&_csrf_token=${login_csrf}') ->header('Content-Type', 'application/x-www-form-urlencoded') ->validateStatus('login redirect', 302); $html->step('Cart page (proves the session)') ->get("$base/cart") ->validateStatus('logged-in cart ok', 200); $html->step('Product page') ->get("$base/products/\${product_id}") ->extractFromHtml('form_csrf', 'input[name="add_to_cart[_token]"]', 'value'); $html->step('Add to cart form') ->post("$base/cart/add", 'add_to_cart[product_id]=${product_id}&add_to_cart[quantity]=${quantity}&add_to_cart[_token]=${form_csrf}') ->header('Content-Type', 'application/x-www-form-urlencoded') ->validateStatus('added, redirect to cart', 302); $html->step('Cart page') ->get("$base/cart") ->extractFromHtml('checkout_csrf', 'form.checkout-form input[name="_token"]', 'value') ->extractFromHtml('logout_csrf', 'form[action="/logout"] input[name="_csrf_token"]', 'value'); $html->step('Checkout form') ->post("$base/checkout", '_token=${checkout_csrf}') ->header('Content-Type', 'application/x-www-form-urlencoded') ->validateStatus('order placed', 302); $html->step('Logout') ->post("$base/logout", '_csrf_token=${logout_csrf}') ->header('Content-Type', 'application/x-www-form-urlencoded') ->validateStatus('logged out', 302); ``` Four Symfony specifics are hiding in there: - **Two token names.** The security component's login form uses `_csrf_token` with token id `authenticate`. A Form-component form nests its token under the form name, so the add-to-cart field is `add_to_cart[_token]`. `extractFromHtml` takes a CSS selector and an attribute, which is cleaner than the regex you would write in JMeter. - **302 on success and on failure.** Symfony redirects to the target on a good login and back to `/login` on a bad one. Both are 302, so validating the login POST proves nothing. The next step requests `/cart`, which answers 200 with a session and 302 to `/login` without one. That is the real assertion. - **The engine never follows redirects.** That is deliberate. Following them would hide the 302 you want to assert on and double-count requests. - **The scenario logs out.** A virtual user keeps its cookie jar for the whole run. Without the logout step, the second iteration's `GET /login` answers 302 because the user is still signed in, and the whole iteration cascades into failures. Because each VoltTest virtual user keeps its cookie jar across iterations, log out explicitly, or clear the cookies, whenever an iteration is supposed to start unauthenticated. ### Data-driven users, and why each scenario gets its own shard Each CSV row carries `email,password,product_id,quantity`, so traffic spreads across the catalog instead of hammering product number one. The rows are handed out in `unique` mode, one per iteration. The first version of this test gave all scenarios the same CSV. Every run had a few percent of unexplained 409s and empty carts. The cause: the API checkout scenario and the HTML shopper both started at row one, so the same account was mid-flow in two scenarios at once, and one checkout emptied the other's cart. The fix is a console command that exports disjoint shards, one per scenario, from a pool of 30,000 seeded users. If your accounts are shared state, your scenarios must not share accounts. ```bash php bin/console app:export-users loadtest/data/users.csv # writes users-1.csv, users-2.csv, users-3.csv, users-flash-sale.csv ``` ### The flash-sale scenario ```php title="loadtest/symfony-shop-test.php (flash sale)" $flash = $test->scenario('API: flash sale (oversell guard)') ->setWeight(15) ->setDataSourceConfiguration($users('flash-sale')); $flash->step('Login (JWT)') ->post("$base/api/v1/login", '{"email":"${email}","password":"${password}"}') ->header('Content-Type', 'application/json') ->validateStatus('login ok', 200) ->extractFromJson('token', 'token'); $flash->step('Add flash-sale item') ->post("$base/api/v1/cart/items", '{"product_id":${product_id},"quantity":1}') ->header('Content-Type', 'application/json') ->header('Authorization', 'Bearer ${token}') ->validateStatus('cart item created', 201); // No validateStatus on purpose: 201 until sold out, then 409 is the *correct* answer. $flash->step('Checkout') ->post("$base/api/v1/checkout", '{}') ->header('Content-Type', 'application/json') ->header('Authorization', 'Bearer ${token}'); ``` Every row in that shard points at the one product with 500 units. Once it sells out, 409 is right, so the checkout step carries no status assertion. The pass/fail check lives in the database instead, and the [flash sale section](#the-flash-sale-proving-checkout-never-oversells) shows it. ## Running It: Local First, Then VoltTest Cloud Run the whole Symfony load test against Docker Compose before touching the cloud. A 30-second run at 20 virtual users finds every script bug, and it is free. ```bash TARGET_URL=http://localhost:8088 VUS=20 DURATION=30s php symfony-shop-test.php ``` ```text Total Reqs: 405 Success Rate: 100.00% Req/sec: 13.28 Median: 2.919ms P95: 380.159ms P99: 396.287ms ``` Even that tiny run tells a story: a 3 ms median with a 380 ms p95 is the login step, and nothing else, because bcrypt is the only slow thing in the app at 20 users. The cloud run is the same file with an API key and a region split: ```php title="loadtest/symfony-shop-test.php (cloud)" if ($apiKey = getenv('VOLTTEST_API_KEY')) { $test->cloud($apiKey)->regions(['us-east-1' => 100]); $run = $test->run(); echo "Dashboard: {$run->getDashboardUrl()}\n"; } ``` ```bash TARGET_URL=https://your-symfony-app.fly.dev VOLTTEST_API_KEY=... STAGES="1m:50,1m:100,1m:200,1m:400,1m:800" php symfony-shop-test.php ``` That staged ramp is the profile every result below was measured with; the script maps `STAGES` onto the SDK's `stage()` calls instead of `setVirtualUsers()`. For a constant load use `VUS=200 DURATION=5m` instead. VoltTest provisions engines in the regions you name, runs the test, and streams results to a dashboard with per-scenario and per-step breakdowns. See the [cloud mode docs](https://docs.volt-test.com/docs/cloud-mode) for staged load profiles and multi-region splits. ## Symfony Load Testing Results: Where a 2-Core Box Saturates Method: Symfony load testing setup: Symfony 7.4.18 on PHP 8.4.25, FrankenPHP 1.12.7 in worker mode at its defaults (4 worker threads, two per vCPU, plus one non-worker thread; the worker count was not set explicitly), PostgreSQL 18.6 (Fly Postgres, single node), Redis 7 on its own Fly machine, the app on one Fly.io `performance-2x` machine (2 dedicated vCPUs, 4 GB) in `iad`. Load: VoltTest Cloud engines in `us-east-1`, staged 50, 100, 200, 400 and 800 [virtual users](/glossary/virtual-users), one minute per stage, [think time](/glossary/think-time) 1 to 2 s, scenario weights 50/20/30/15 as above, 4,000 accounts per scenario shard. All runs on 2026-09-06; the raw result files ship with the repository. The first Symfony load test was the number everyone wants to quote: 2,000 users for five minutes. It produced nothing usable, with Fly's proxy capped at 1,000 concurrent requests in front of the app at the time (the published repository's `fly.toml` now sets the soft and hard limits to 2,000 and 4,000, the values every later run used): 93% of requests hit the engine's 30-second timeout, and every step's p50 was 30,015 ms, which is the timeout, not the app. A run that only reports timeouts tells you the number is too big, not what the number is. So the second run ramped instead, one minute at each of 50, 100, 200, 400 and 800 users, to find where the app actually bends. | Stage | Users | Requests/s | Errors | p50 | [p95](/glossary/percentiles-p95-p99) | |---|---|---|---|---|---| | 1 | 50 | 13.3 | 0% | 433 ms | 1.1 s | | 2 | 100 | 19.5 | 0% | 2.2 s | 3.1 s | | 3 | 200 | 11.0 | 0% | 10.9 s | 13.7 s | | 4 | 400 | 13.9 | 1.6% | 15.9 s | 18.8 s | | 5 | 800 | 16.3 | 74% | 27.6 s | timeout | ![Staged Symfony load test: throughput flattens at 100 virtual users while response time keeps climbing](/img/symfony-run-overview.png) Throughput tops out near 20 requests per second around 100 users and never grows again. After that, every additional user only lengthens the queue: the p50 climbs from 2 seconds at 100 users to 11 at 200 and 16 at 400, then the timeouts start. Below about 15 users the app is quick, with a 10 ms median and a 500 ms p95 that is purely the login step. The cause is arithmetic. The run attempted 1,128 logins in five minutes, 3.8 per second, and a bcrypt cost-13 verify takes 479 ms on this CPU, so two cores can do at most about four of them per second. The machine spent nearly all of its CPU hashing passwords; the [saturation point](/glossary/saturation-point) was the login step, not Doctrine, not sessions, not Symfony. ### How many concurrent users can a Symfony app handle? There is no general number. This authentication-heavy workload, where every iteration logs in, reached its knee at about 100 virtual users on this 2-vCPU machine at bcrypt cost 13, and at about 400 once the cost dropped to 10, as the tuning section shows. The limit in this Symfony load test was CPU spent on password hashing, not the framework, and a workload that logs in once and then browses would land somewhere else entirely. That is why you measure your own app instead of quoting someone else's number. ## FrankenPHP Worker Mode vs PHP-FPM: Same App, Same Test The comparison everyone quotes is 1,240 requests per second on nginx and php-fpm against 3,850 on FrankenPHP for a JSON endpoint on a t3.medium. Those numbers are real, and they measure bootstrap cost, because bootstrap is nearly all a hello-world endpoint does. To see what worker mode does for an app with a database, sessions and password hashing, the same shop was rebuilt on nginx and php-fpm with the same PHP version, opcache and preload settings, and a static php-fpm pool of `pm.max_children = 5`, sized to FrankenPHP's total PHP thread count on this 2-vCPU machine (four worker threads plus one non-worker thread). FrankenPHP's worker count was left at its default of two per CPU, so php-fpm actually had five request-serving processes against FrankenPHP's four workers. If that skews anything, it skews in php-fpm's favour. The image was deployed to the same Fly app with the same PostgreSQL and Redis, and the identical test ran again. | Same staged ramp | php-fpm + nginx | FrankenPHP worker | Change | |---|---|---|---| | Requests served in 5 minutes | 3,942 | 4,477 | +14% | | Peak requests/s | 40 | 61 | 1.5x | | Requests/s at 50 users | 12.5 | 13.3 | +6% | | Requests/s at 100 users (the knee) | 13.6 | 19.5 | +43% | | p50 at 50 users | 588 ms | 433 ms | 1.4x faster | | Product page p50 under load | 10.7 s | 4.0 s | 2.7x faster | | Cart page p50 under load | 6.1 s | 3.1 s | 2x faster | | Login p50 under load | 14.5 s | 12.7 s | 1.1x faster | | Errors at 400 users | 8.6% | 1.6% | | | 800 users | collapse | collapse | same wall | ![Compare view: the same Symfony load test on php-fpm and on FrankenPHP worker mode, worker mode ahead until both saturate](/img/symfony-frankenphp-vs-fpm.png) Before the cloud runs, the same comparison on a laptop (Docker Desktop, 4 cores, 100 virtual users, 45 seconds, the three everyday scenarios) already shows the shape of the answer: | Local preview, 100 VUs | php-fpm + nginx | FrankenPHP worker | Change | |---|---|---|---| | Single product read, idle server | 12 ms | 2 ms | 6x faster | | Requests per second | 44.2 | 48.5 | +10% | | Median response | 507 ms | 312 ms | 1.6x faster | | p95 | 1.65 s | 1.60 s | about the same | A sixfold gain on the read that is nothing but bootstrap, and ten percent on the mix, because the mix spends its time in bcrypt and PostgreSQL where worker mode has nothing to remove. The Symfony load testing results in the cloud matched the laptop pattern exactly. The pages that are mostly bootstrap, the product and cart pages, ran two to three times faster under worker mode even while queueing. The login step, which is 479 ms of bcrypt plus a few milliseconds of framework, barely moved. Overall FrankenPHP served 14% more requests and reached 1.5 times the peak, and then both runtimes collapsed at 800 users for the same reason. Worker mode removes the repeated bootstrap; it cannot remove password hashing. ### Is FrankenPHP faster than php-fpm for Symfony? Yes, and by how much depends on the page. In this Symfony load testing comparison worker mode served 14% more requests overall, up to 2.7x more on the bootstrap-heavy product and cart pages, and almost nothing extra on login, where bcrypt dominates. Avoiding the repeated framework bootstrap appears to account for much of the improvement on the lightweight endpoints, and a persistent process changes other things too, such as connection reuse and warm caches, that this test did not isolate. Measure your own endpoints before promising anyone a multiplier. ## The Flash Sale: Proving Checkout Never Oversells A Symfony load test is not only about speed. This one also answers a correctness question you cannot answer with PHPUnit: when 500 units exist and thousands of buyers race for them, does the app sell exactly 500? The checkout service reserves stock with one statement inside the order transaction: ```php title="src/Repository/ProductRepository.php" public function reserveStock(int $productId, int $quantity): bool { $affected = $this->getEntityManager()->getConnection()->executeStatement( 'UPDATE product SET stock = stock - :qty WHERE id = :id AND stock >= :qty', ['qty' => $quantity, 'id' => $productId] ); return $affected === 1; } ``` Zero affected rows means sold out, the service throws, the transaction rolls back, and the API answers 409. There is no explicit lock in that code, and it still cannot oversell, for two reasons. **PostgreSQL locks the row for you.** Every `UPDATE` holds a lock on the rows it modifies until the transaction ends. Run two checkouts by hand, leaving the first transaction open, and the second shows up in `pg_stat_activity` as `active | Lock | transactionid`: it is waiting for the first transaction to finish. **The condition is re-checked after the wait.** Under `READ COMMITTED`, a blocked update does not use the row it saw before waiting. When the first transaction commits with stock at 0, the second re-evaluates `stock >= 1` against the new row, matches nothing, and returns zero rows. Check and decrement are one atomic statement, so there is no window between reading the stock and writing it. ![Two concurrent checkouts for the last unit: the first UPDATE locks the row, the second waits, re-checks stock and gets 409](/img/symfony-checkout-lock.png) Remove the `AND stock >= :qty` and the second buyer still waits on the lock, then writes `-1`. Locks serialise writes; they do not validate them. A `CHECK (stock >= 0)` constraint on the table catches that case as a hard error, which the app also carries as a backstop, but the conditional update stays the primary guard because it produces a clean 409 instead of a 500. The run itself, 100 virtual users on the flash-sale scenario alone for 90 seconds: ```text title="bin/console app:flash-sale report" -------------------------------- ------- metric value -------------------------------- ------- product id 1001 initial stock 500 stock now 0 units sold since last reset 500 orders containing it (all time) 689 units sold (all time) 689 units still sitting in carts 74 -------------------------------- ------- [OK] Sold out, never oversold. ``` The all-time rows span earlier runs; the row that matters is the 500 sold since the reset against a stock that ended at exactly 0. That was the local run. The cloud version of this Symfony load test put 500 virtual users on the flash-sale scenario alone for two minutes, against the Fly box with cost-10 hashes. They made 6,431 requests at 54 per second, peaking at 116, with 6 failures in total. Checkout was called 1,969 times, wrote exactly 500 orders, and answered 409 to the rest. Stock finished at 0. And 1,715 units were still sitting in the losers' carts, which is what a real flash sale looks like the morning after. ![Flash-sale run at 500 virtual users: checkouts succeed until the 500 units are gone, then answer 409](/img/symfony-flash-sale-run.png) The cost of correctness is visible in the latency: a p50 of 6.1 seconds and a p95 of 17.8 seconds. With five PHP workers, the queue formed in FrankenPHP before it ever reached the row lock; the database sampler never caught a transaction waiting, because at most five could be inside the database at once. The row lock is what made the result correct. The worker pool is what made it slow. That split is why flash sales need a different design than the everyday checkout path, reserving stock from a counter in Redis or queueing the orders, and the load test is what makes the queue visible before your customers do. One more trap the test exposed: carts with several products lock several rows. Two carts holding the same products in opposite order can deadlock, and PostgreSQL will abort one of them after a second. The fix is to lock in a fixed order, which is one `usort` by product id before the loop. It is in the app now. ## Tuning What the Numbers Point At Every Symfony load testing result above points at a specific knob. Change one at a time and rerun. **Password hash cost.** Symfony's `auto` hasher chose bcrypt cost 13. On the Fly box a single verify costs 479 ms at cost 13 and 60 ms at cost 10, so dropping the cost lifts the login ceiling roughly eightfold. Whether that is acceptable is a security decision, not a performance one. OWASP's [password storage guidance](https://cheatsheetseries.owasp.org/cheatsheets/Password_Storage_Cheat_Sheet.html) puts the bcrypt floor at cost 10 and prefers Argon2id outright, which Symfony supports through the `sodium` hasher; that would trade CPU time for memory, and it is a different experiment. The rerun with cost 10, same box, same staged ramp, everything else equal: | Users | Cost 13 | Cost 10 | |---|---|---| | 50 | 13 req/s, p50 433 ms | 18 req/s, p50 12 ms | | 100 | 20 req/s, p50 2.2 s | 53 req/s, p50 15 ms | | 200 | 11 req/s, p50 10.9 s | 102 req/s, p50 109 ms | | 400 | 14 req/s, 1.6% errors, p50 15.9 s | 118 req/s, 0% errors, p50 1.1 s | | 800 | 74% timeouts | 77 req/s, 0% errors, p50 5.2 s | ![Compare view of the staged Symfony load test at bcrypt cost 13 versus cost 10: five times the throughput at cost 10](/img/symfony-bcrypt-cost-13-vs-10.png) Five times the requests over the run, 22,195 against 4,477, zero errors, and the knee moved from about 100 users to about 400. Logins went from 3.8 to 15.4 per second. That is what one setting is worth, once the stored hashes match it and the upgrader is in place, when a load test has shown you where the CPU goes. There is a trap on the way there, and the load test is what exposed it. I re-hashed all 30,000 users at cost 10 and measured a login from inside the machine: **547 ms**, barely better than before. The hash itself now took 60 ms and a product read took 3.5 ms, so 480 ms were unaccounted for. The missing time was Symfony's password migration. The hasher was still configured for cost 13, so on every login `PasswordMigratingListener` saw a hash that "needs rehash" and computed a fresh cost-13 hash to upgrade it. It then handed that hash to a user repository that did not implement `PasswordUpgraderInterface`, which silently dropped it. Every login paid for both costs and stored neither. Two lines fix it: put the cost in configuration so it matches what is stored, and implement `PasswordUpgraderInterface` on the repository so migrations actually land. ```php title="src/Repository/UserRepository.php" class UserRepository extends ServiceEntityRepository implements PasswordUpgraderInterface { public function upgradePassword(PasswordAuthenticatedUserInterface $user, string $newHashedPassword): void { if (!$user instanceof User) { throw new UnsupportedUserException(sprintf('Instances of "%s" are not supported.', $user::class)); } $user->setPassword($newHashedPassword); $this->getEntityManager()->flush(); } } ``` ```yaml title="config/packages/security.yaml" security: password_hashers: Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface: algorithm: auto cost: '%env(int:PASSWORD_HASH_COST)%' ``` **Log in once per session.** A test that logs in on every iteration is a login benchmark. Real users log in once and browse for minutes. Shift the scenario to one login followed by ten browsing iterations and the login share of CPU drops accordingly. Model the traffic you have, not the flow that is easiest to script. **Sessions in Redis with a TTL.** Already done in the test app, and the reason the HTML scenario scaled without a lock convoy. Set a TTL so 30,000 test users do not leave 30,000 keys behind. Remember that Symfony's `RedisSessionHandler` does not lock; if concurrent requests write to the same session, switch to the phpredis native handler with locking enabled and re-measure. **Doctrine result cache for the catalog.** Product pages are read-heavy and change rarely. A result cache on the catalog query turns a 2 ms page into a fraction of that, which matters more under worker mode where bootstrap is already gone. **Worker count.** FrankenPHP defaults to two worker threads per CPU (plus one thread for non-worker requests) and this test left that default alone; set `num` in the `worker` block to change it. On a login-bound workload more workers only deepen the CPU queue; on a database-bound workload more workers hide latency. The Fly CPU graph from run 1 is the evidence for which side you are on. ## Automating Symfony Load Tests in CI The core SDK returns a result object, so a Symfony load test can be a PHPUnit test with thresholds, and a failed threshold fails the pipeline. ```php title="tests/Performance/ShopLoadTest.php" use PHPUnit\Framework\TestCase; use VoltTest\VoltTest; final class ShopLoadTest extends TestCase { public function testShopStaysUnderBudget(): void { // The script returns the configured test when it is included instead of run. /** @var VoltTest $test */ $test = require __DIR__.'/../../loadtest/symfony-shop-test.php'; $result = $test->setVirtualUsers(50)->setDuration('1m')->run(); self::assertGreaterThanOrEqual(99.5, $result->getSuccessRate()); self::assertLessThan(500, self::toMilliseconds($result->getP95ResponseTime())); } /** The SDK reports durations as strings such as "380.159ms" or "1.599487s". */ private static function toMilliseconds(?string $duration): float { return match (true) { $duration === null => INF, str_ends_with($duration, 'ms') => (float) $duration, str_ends_with($duration, 'µs') => (float) $duration / 1000, str_ends_with($duration, 's') => (float) $duration * 1000, default => (float) $duration, }; } } ``` The script's own run block is guarded so `require` hands back the configured test without starting it: ```php title="loadtest/symfony-shop-test.php (tail)" if (!isset($argv[0]) || realpath($argv[0]) !== __FILE__) { return $test; // included from PHPUnit or another script } ``` Run it against a compose stack in GitHub Actions, gate merges on it, and rerun the full cloud test before releases. The [Laravel PHPUnit integration](/blog/laravel-load-testing-with-phpunit) shows the same pattern with the Laravel package's `assertVT*` helpers; for Symfony you assert on the result object directly. ## What to Do Next 1. Put your app in production mode locally: `APP_ENV=prod`, opcache preload, Redis sessions. A load test of a dev container measures the profiler. 2. Write the HTML scenario first. If CSRF, sessions and logout work under load, the API scenario is easy. 3. Give each scenario its own accounts, then run 20 users for 30 seconds until the success rate is 100%. 4. Only then scale up, and change one variable per run. That is the whole method behind every Symfony load testing number in this guide. ## Resources - [VoltTest documentation](https://docs.volt-test.com/docs/introduction), [steps and extractors](https://docs.volt-test.com/docs/Steps), [scenarios and data sources](https://docs.volt-test.com/docs/Scenarios), [HTML form examples](https://docs.volt-test.com/docs/Examples/html-form-examples), [JSON API examples](https://docs.volt-test.com/docs/Examples/JSON-API-Examples), [cloud mode](https://docs.volt-test.com/docs/cloud-mode) - [The Symfony shop and load test used in this guide](https://github.com/volt-test/symfony-load-testing-example), including the raw result files of every run - [Symfony docs: CSRF protection](https://symfony.com/doc/current/security/csrf.html), [session configuration](https://symfony.com/doc/current/session.html), [password hashing](https://symfony.com/doc/current/security/passwords.html), [FrankenPHP with Symfony](https://frankenphp.dev/docs/symfony/) - [PostgreSQL docs: row-level locks and READ COMMITTED](https://www.postgresql.org/docs/current/transaction-iso.html) - Related guides: [How to load test Laravel applications](/blog/laravel-load-testing), [PHP load testing](/blog/php-load-testing), [performance testing types](/blog/performance-testing-types) VoltTest's PHP SDK is open source on [GitHub](https://github.com/volt-test/php-sdk). If this guide saved you a bad launch, a star helps other Symfony developers find it. --- ### How to Load Test Laravel Applications: Tools, Setup, and Real Results URL: https://volt-test.com/blog/laravel-load-testing Published: 2026-07-13 Keywords: laravel load testing, how to load test laravel, laravel performance testing, laravel stress testing, load test laravel api, laravel load testing tools, laravel concurrent users, laravel csrf load test, laravel sanctum load test, pest stressless vs volttest, k6 laravel, volttest php sdk # How to Load Test Laravel Applications: Tools, Setup, and Real Results More than seven years ago, I shipped a Laravel API that passed every unit test, every feature test, and every browser test I threw at it. The staging environment ran fine. Code review was clean. I deployed on a Friday afternoon, and by Monday morning, the app was returning 502s under 200 concurrent users. The database connection pool was exhausted. File-based sessions were creating lock contention. A queued job that ran fine in isolation was stacking up 4,000 pending jobs because the worker couldn't keep pace with incoming requests. None of these problems showed up in testing because I'd never tested with more than one user at a time. The company's first answer was to hire an external firm to run the load tests. The results were useful, but they arrived as a report from people who didn't know our codebase, and we couldn't rerun them after every change. So we brought load testing in-house with tools like k6, and got familiar with them ourselves. That exposed the real cost: the tooling lived outside PHP, so every new engineer we hired had to learn a separate language and toolchain before they could write a single test. With VoltTest, a PHP team goes from zero to a working load test in about two hours at most, because there's nothing new to learn. That experience is why I built [VoltTest](https://docs.volt-test.com), and it's why I'm writing this guide. Laravel load testing isn't optional if you're running anything beyond a personal project. This guide covers why Laravel apps fail under load, how the available tools compare, and how to set up and run real load tests using PHP: no JavaScript, no Python, no context-switching. TL;DR: Laravel load testing catches failures that unit and feature tests miss: database bottlenecks, session contention, queue backlogs. VoltTest lets you write load tests in PHP, handles CSRF and cookies automatically, integrates with PHPUnit for CI/CD gating, and scales from local runs to thousands of concurrent users in the cloud. This guide walks through the complete setup. ![Load test Laravel in pure PHP with VoltTest: CSRF, auth flows and queues handled, with PHPUnit gates for CI/CD](/img/laravel-load-testing-og.png) ## What Breaks in Laravel Under Load Laravel is optimized for developer productivity, not raw throughput. That's a deliberate trade-off, and a fine one, as long as you know where the framework's abstractions become bottlenecks under concurrent traffic. Laravel performance testing reveals bottlenecks that are invisible at low traffic. Here's what I've seen break in production, in order of how often it catches people off guard: ### Database: The First Wall You Hit Eloquent makes database access effortless, which is exactly why it becomes a problem under load. The patterns that work fine for 10 users collapse at 500: - **N+1 queries**: a `foreach` loop that fires 200 individual SELECTs instead of one eager-loaded query. At 100 concurrent users, that's 20,000 queries per second hitting your database. - **Missing indexes**: a `WHERE` clause on an unindexed column scans the full table. Fast at 1,000 rows, catastrophic at 1,000,000. - **Connection pool exhaustion**: PHP opens and closes database connections per-request. Under sustained load, the connection churn can exceed your database's `max_connections` limit, causing new requests to queue or fail. ### Sessions and Authentication File-based sessions (Laravel's default) use file locks. When multiple requests hit the same session (AJAX-heavy pages, SPA polling), they serialize instead of running in parallel. I've seen P95 response times jump from 50ms to 3 seconds just from session lock contention. CSRF token verification adds per-request overhead that's invisible at low traffic but measurable at scale. Sanctum token lookups hit the database on every authenticated request. That's fine at 50 RPS, but potentially a bottleneck at 500. ### Queues and Jobs Your queue workers have a fixed throughput. If incoming jobs arrive faster than workers can process them, the queue grows indefinitely. Load testing reveals whether your worker count matches your actual job generation rate, something you can't determine from unit tests. I've seen a notification system that dispatched 3 jobs per user action (email, Slack, database notification) overwhelm a 2-worker Horizon setup at just 100 concurrent users. The fix was simple (more workers, batching), but the problem only surfaced under load. ### PHP-FPM Worker Saturation Each PHP-FPM worker handles one request at a time. When all workers are busy, new requests queue at the web server level. The default `pm.max_children = 5` on many development setups is nowhere near enough for production traffic. Load testing tells you exactly how many workers you need, and whether switching to [Laravel Octane](https://laravel.com/docs/octane) with its persistent workers would help. ## Laravel Load Testing Tools Compared There's no shortage of load testing tools, but most of them aren't built with PHP or Laravel in mind. Here's an honest comparison of the options a Laravel developer is likely to consider: | Feature | VoltTest | k6 (Grafana) | Pest Stressless | LoadForge | |---|---|---|---|---| | **Test language** | PHP | JavaScript | PHP (wraps k6) | Python (Locust) | | **Install** | `composer require` | Binary download | `composer require` | SaaS / pip | | **Laravel CSRF handling** | Built-in `extractCsrfToken()` | Manual JS parsing | No | Manual Python parsing | | **Cookie/session management** | `autoHandleCookies()` | Manual | No | Manual | | **Multi-step scenarios** | Yes (chained steps with data extraction) | Yes | No (single URL only) | Yes | | **Artisan commands** | `volttest:make`, `volttest:run` | N/A | `pest stress` | N/A | | **PHPUnit integration** | Native (`PerformanceTestCase` + assertions) | No | Via Pest expectations | No | | **Route discovery** | `--routes` flag auto-generates tests | No | No | No | | **Data-driven (CSV)** | Yes (`unique`, `sequential`, `random` modes) | Yes | No | Yes | | **Cloud scaling** | Built-in (30+ regions) | Grafana Cloud k6 | No | Built-in | | **P95/P99 metrics** | Yes | Yes | Yes (via k6) | Yes | | **Staged load profiles** | Yes (spike, stress, soak patterns) | Yes | No (fixed concurrency) | Yes | | **Protocols supported** | HTTP only | HTTP, WebSocket, gRPC, browser | HTTP only | HTTP, plus any Python client | | **Per-step assertions** | Status code only | Arbitrary JS checks | Pest expectations (single URL) | Arbitrary Python | | **Maturity / ecosystem** | New (2025) | Large, Grafana-backed | Built on k6 | Established platform | ### When to Use Each Tool **VoltTest** is the right choice when your team writes PHP, you want tests version-controlled alongside your Laravel codebase, and you need multi-step scenarios with CSRF/auth handling without leaving the PHP ecosystem. It's the only tool with native Laravel Artisan integration and PHPUnit assertions for CI/CD gating. **k6** is excellent if your team is already invested in the Grafana observability stack or prefers JavaScript. Its Go-based engine is fast, and Grafana Cloud k6 scales well. [Laravel.com benchmarked it at 17,000 RPS](https://laravel.com/blog/k6-load-testing-on-laravel-cloud) against Laravel Cloud. But you'll write tests in JavaScript, not PHP — [the full comparison is here](/compare/volttest-vs-k6). **Pest Stressless** is perfect for quick single-URL smoke tests (`./vendor/bin/pest stress example.com --concurrency=5`). It wraps k6 under the hood and integrates with Pest's expectation API. But it can't chain requests, handle CSRF tokens, or simulate multi-step user flows. It's a quick-check tool, not a load testing solution. **LoadForge** works well for teams comfortable with Python/Locust who want a managed cloud platform. Its Laravel-specific guides are solid, but you write tests in Python. ([VoltTest vs LoadForge](/compare/volttest-vs-loadforge).) **Where VoltTest is the weaker choice:** it speaks HTTP and nothing else, so it can't drive a WebSocket, gRPC or queue workload — k6 can. Its only per-step assertion is the status code, so if you need to assert on response bodies mid-scenario, k6's checks are more expressive. And it's the newest tool in this table, with a correspondingly smaller ecosystem and shorter track record. ## Step-by-Step: Load Testing Laravel with VoltTest Everything below uses the [Laravel package](/solutions/laravel-load-testing) — Artisan commands, route scaffolding and session handling included. Let me walk through how I set up load testing for a Laravel application, from install to results. ### Install the Laravel Package ```bash composer require volt-test/laravel-performance-testing --dev php artisan vendor:publish --tag=volttest-config ``` This gives you Artisan commands, automatic CSRF handling, cookie management, and PHPUnit integration. The core [VoltTest PHP SDK](https://docs.volt-test.com/docs/introduction) is pulled in as a dependency. ### Create Your First Test ```bash php artisan volttest:make LoginTest ``` This scaffolds `app/VoltTests/LoginTest.php`. Here's a complete login flow that handles CSRF tokens automatically: ```php title="app/VoltTests/LoginTest.php" target('http://localhost:8000'); $scenario = $manager->scenario('Login Flow'); // Step 1: Load login page, extract CSRF token $scenario->step('Get Login Page') ->get('/login') ->expectStatus(200) ->extractCsrfToken(); // Step 2: Submit login with extracted token // Cookies are handled automatically, no manual session management $scenario->step('Submit Login') ->post('/login', [ '_token' => '${csrf_token}', 'email' => 'user@example.com', 'password' => 'password', ]) ->expectStatus(302) ->thinkTime('1s'); // Step 3: Verify authenticated access $scenario->step('Load Dashboard') ->get('/dashboard') ->expectStatus(200); } } ``` `extractCsrfToken()` reads the `_token` hidden input from the HTML response and stores it as `${csrf_token}` for subsequent steps. Cookies, including the `laravel_session` cookie, are passed between steps automatically. No manual header juggling. ### Run the Test ```bash php artisan volttest:run LoginTest --users=50 --duration=1m ``` For more control over how users are added over time, use staged load profiles: ```bash php artisan volttest:run LoginTest --users=50 --duration=1m --ramp-up=15s ``` Or define stages directly in your test for spike or stress patterns: ```php public function define(VoltTestManager $manager): void { $manager->target('http://localhost:8000'); // Ramp up, hold, spike, recover, ramp down $manager->stage('1m', 50); // Ramp to 50 VUs $manager->stage('5m', 50); // Hold steady $manager->stage('10s', 200); // Spike to 200 $manager->stage('2m', 200); // Hold spike $manager->stage('1m', 0); // Ramp down $scenario = $manager->scenario('Login Flow'); // ... steps as above } ``` See the [load profiles guide](https://docs.volt-test.com/docs/load-profiles) for spike, stress, soak, and step-up patterns. ## Testing Authentication Flows Under Load Authentication is where most load testing tools struggle with Laravel. CSRF tokens, session cookies, and Sanctum tokens all require extracting values from responses and passing them to subsequent requests. VoltTest handles both web and API authentication patterns. ### Web Routes: CSRF + Session Cookies For traditional Laravel web routes (Blade forms, Inertia, Livewire), VoltTest automatically handles the `laravel_session` cookie and provides `extractCsrfToken()`: ```php $scenario = $manager->scenario('Registration Flow') ->dataSource('users.csv', 'unique'); $scenario->step('Registration Page') ->get('/register') ->expectStatus(200) ->extractCsrfToken() ->thinkTime('3s'); $scenario->step('Submit Registration') ->post('/register', [ '_token' => '${csrf_token}', 'name' => '${name}', 'email' => '${email}', 'password' => '${password}', 'password_confirmation' => '${password}', ]) ->expectStatus(302); $scenario->step('Verify Dashboard Access') ->get('/dashboard') ->expectStatus(200); ``` Each virtual user gets unique credentials from the CSV file, so sessions don't collide. ### API Routes: Sanctum Token Auth For API endpoints using Laravel Sanctum or Passport, extract the token from the JSON response: ```php $scenario = $manager->scenario('API Auth Flow'); $scenario->step('Login API') ->post('/api/login', [ 'email' => 'user@example.com', 'password' => 'password', ], ['Content-Type' => 'application/json', 'Accept' => 'application/json']) ->expectStatus(200) ->extractJson('token', 'data.token'); $scenario->step('Get User Profile') ->get('/api/user') ->header('Authorization', 'Bearer ${token}') ->header('Accept', 'application/json') ->expectStatus(200) ->extractJson('user_id', 'data.id'); $scenario->step('Update Profile') ->put('/api/user/${user_id}', [ 'name' => 'Updated Name', ], ['Content-Type' => 'application/json', 'Authorization' => 'Bearer ${token}']) ->expectStatus(200); ``` The `extractJson('token', 'data.token')` call captures the token from the login response and makes it available as `${token}` in all subsequent steps. This works with any JSON structure: nested paths like `data.user.api_token` and array indexing like `data[0].token` are both supported. ## Testing Queues, Jobs, and Broadcasting Under Load Authentication isn't the only Laravel-specific concern. Queue-heavy applications (and most non-trivial Laravel apps dispatch jobs) need load testing that exercises the full request-to-job pipeline. The pattern is straightforward: your load test hits endpoints that dispatch jobs, and you monitor whether the queue keeps up or falls behind. VoltTest doesn't test queue workers directly (that's a monitoring concern; [Laravel Horizon](https://laravel.com/docs/horizon) and [Pulse](https://laravel.com/docs/pulse) handle it). But it does tell you how fast your application *generates* jobs under concurrent load, which is the data you need to size your worker pool. Here's a real scenario: an e-commerce app where every order dispatches three jobs: a confirmation email, an inventory update, and a webhook notification to a fulfillment service. ```php title="app/VoltTests/OrderLoadTest.php" target('http://localhost:8000'); $scenario = $manager->scenario('Order Flow') ->dataSource('customers.csv', 'unique'); $scenario->step('Login API') ->post('/api/login', [ 'email' => '${email}', 'password' => '${password}', ], ['Content-Type' => 'application/json']) ->expectStatus(200) ->extractJson('token', 'data.token'); $scenario->step('Add to Cart') ->post('/api/cart', [ 'product_id' => 42, 'quantity' => 1, ], [ 'Authorization' => 'Bearer ${token}', 'Content-Type' => 'application/json', ]) ->expectStatus(200) ->thinkTime('2s'); $scenario->step('Place Order') ->post('/api/orders', [ 'payment_method' => 'card_test', 'shipping_address_id' => 1, ], [ 'Authorization' => 'Bearer ${token}', 'Content-Type' => 'application/json', ]) ->expectStatus(201); } } ``` Run this at 100 concurrent users for 5 minutes and then check Horizon: how many pending jobs accumulated? If the queue depth grew steadily throughout the test, your workers can't keep up with the job generation rate at that concurrency level. The fix is usually more workers, job batching, or moving non-critical jobs (analytics, logging) to a separate lower-priority queue. This is the kind of problem that never appears in unit or feature tests. It only surfaces when the *rate* of job dispatch exceeds worker throughput, which requires concurrent load to trigger. ## Interpreting Your Results: P95, P99, and What They Mean When I run a load test, the output looks like this: ```text Test Metrics Summary: =================== Duration: 60.003s Total Reqs: 12847 Success Rate: 99.92% Req/sec: 214.11 Success Requests: 12837 Failed Requests: 10 Response Time: ------------ Min: 8.21ms Max: 1.247s Avg: 94.31ms Median: 72.18ms P95: 182.65ms P99: 341.20ms ``` Here's what matters and what doesn't: | Metric | What It Tells You | When to Worry | |---|---|---| | **Success Rate** | Percentage of non-error responses | Below 99% under expected load | | **Req/sec (RPS)** | Actual throughput | Below your expected peak traffic | | **P95** | 95% of users experience this latency or better | Above 200ms for API routes, 500ms for web pages | | **P99** | The worst 1% of user experiences | More than 3x the P95 (indicates intermittent bottlenecks) | | **Avg** | Statistical average that hides outliers | Avoid making decisions on this alone | | **Max** | Single worst response | Above 5 seconds (timeouts incoming) | **The P95/P99 gap is your most important diagnostic signal.** If P95 is 180ms but P99 is 2 seconds, you have intermittent slowdowns, usually database lock contention, garbage collection pauses, or a slow external API call that occasionally blocks. If P95 and P99 are close together, your performance is consistent. **Run at increasing VU counts** (50, 100, 200, 500) and plot P95 against VU count. The inflection point, where P95 starts climbing sharply, is your app's capacity ceiling. Everything above that line requires optimization or horizontal scaling. ### What "Good" Looks Like for Laravel These aren't universal rules; your thresholds depend on app complexity, database size, and infrastructure. But as starting benchmarks from my experience testing dozens of Laravel apps: | Endpoint Type | Target P95 | Target Success Rate | Notes | |---|---|---|---| | Static/cached API | < 50ms | > 99.9% | Redis or response cache should absorb most of the work | | Authenticated API (CRUD) | < 200ms | > 99.5% | Database queries are the usual bottleneck | | Web pages (Blade/Inertia) | < 500ms | > 99% | Includes view rendering and asset pipeline overhead | | Complex operations (reports, exports) | < 2s | > 95% | Heavy queries are expected, so set realistic thresholds | If your numbers are significantly worse than these baselines under moderate load (50-100 VUs), you likely have a query optimization issue, not an infrastructure one. Start with `php artisan telescope` or query logging before scaling hardware. ## Automating Load Tests in CI/CD Performance regressions are silent: they don't trigger test failures unless you wire up assertions. VoltTest's PHPUnit integration turns load tests into automated quality gates. ### PHPUnit Performance Assertions ```php title="tests/Performance/ApiLoadTest.php" runVoltTest(new LoginTest(), [ 'virtual_users' => 30, 'duration' => '30s', ]); // Fail the build if performance degrades $this->assertVTSuccessful($result, 99.0); $this->assertVTP95ResponseTime($result, 200); $this->assertVTP99ResponseTime($result, 500); $this->assertVTMinimumRPS($result, 100); $this->assertVTErrorRate($result, 1.0); } } ``` Available assertions: | Assertion | What It Checks | |---|---| | `assertVTSuccessful($result, 99.0)` | Success rate ≥ 99% | | `assertVTP95ResponseTime($result, 200)` | P95 latency ≤ 200ms | | `assertVTP99ResponseTime($result, 500)` | P99 latency ≤ 500ms | | `assertVTMinimumRPS($result, 100)` | Throughput ≥ 100 req/s | | `assertVTErrorRate($result, 1.0)` | Error rate ≤ 1% | | `assertVTAverageResponseTime($result, 150)` | Avg latency ≤ 150ms | | `assertVTMedianResponseTime($result, 100)` | Median latency ≤ 100ms | ### GitHub Actions Workflow Here's a working workflow that runs load tests on every pull request and fails the build on performance regression: ```yaml title=".github/workflows/load-test.yml" name: Performance Tests on: pull_request: branches: [main] jobs: load-test: runs-on: ubuntu-latest services: mysql: image: mysql:8.0 env: MYSQL_DATABASE: testing MYSQL_ROOT_PASSWORD: password ports: - 3306:3306 options: --health-cmd="mysqladmin ping" --health-interval=10s --health-timeout=5s --health-retries=3 redis: image: redis:7 ports: - 6379:6379 steps: - uses: actions/checkout@v4 - name: Setup PHP uses: shivammathur/setup-php@v2 with: php-version: '8.3' extensions: pdo_mysql, redis, pcntl coverage: none - name: Install dependencies run: composer install --no-interaction --prefer-dist - name: Prepare environment run: | cp .env.testing .env php artisan key:generate php artisan migrate --seed - name: Start application server run: php artisan serve --host=127.0.0.1 --port=8000 & env: DB_CONNECTION: mysql DB_HOST: 127.0.0.1 DB_PORT: 3306 DB_DATABASE: testing DB_USERNAME: root DB_PASSWORD: password - name: Wait for server run: | for i in $(seq 1 30); do curl -s http://127.0.0.1:8000 > /dev/null && break sleep 1 done - name: Run performance tests run: vendor/bin/phpunit --testsuite=Performance env: VOLTTEST_BASE_URL: http://127.0.0.1:8000 ``` Add the performance test suite to your `phpunit.xml`: ```xml tests/Performance ``` Now every PR runs a load test. If P95 latency exceeds 200ms or the success rate drops below 99%, the build fails, and the developer sees exactly which metric regressed. ## Scaling to Thousands of Users with VoltTest Cloud Local testing is limited by your machine's hardware. My laptop can drive a few hundred virtual users comfortably; beyond that, the load generator itself becomes the bottleneck. VoltTest Cloud runs your tests on managed infrastructure across 30+ AWS regions. The code doesn't change. You add one line: ```php $manager->cloud(); ``` Or from the CLI: ```bash php artisan volttest:run LoginTest --users=5000 --duration=5m --cloud ``` For multi-region distribution: ```php $manager->cloud(); $manager->regions([ 'us-east-1' => 60, // 60% of VUs in Virginia 'eu-west-1' => 40, // 40% in Ireland ]); ``` The cloud dashboard shows real-time metrics (RPS, latency percentiles, error rates, and per-step breakdowns) as the test runs. Results are stored for comparison across test runs. Try VoltTest Cloud Free: Run distributed load tests at scale without managing infrastructure — 500 VUs free, no credit card. ## What to Do Next 1. **Install the package**: `composer require volt-test/laravel-performance-testing --dev` 2. **Test your login flow**: it's the most common bottleneck and the easiest to test first 3. **Run at increasing VU counts**: 10, 50, 100, 200. Watch where P95 inflects 4. **Add PHPUnit assertions**: wire performance tests into your CI pipeline so regressions never reach production 5. **Try cloud mode**: when you need thousands of concurrent users or multi-region testing Laravel performance testing isn't about generating impressive throughput numbers. It's about knowing, before your users tell you, whether your app handles the traffic you expect. ## Learn More - [Laravel Package Documentation →](https://docs.volt-test.com/docs/laravel/laravel-installation) - [VoltTest PHP SDK Documentation →](https://docs.volt-test.com/docs/introduction) - [Load Testing Laravel with PHPUnit (deep dive) →](/blog/laravel-load-testing-with-phpunit) - [Load Profiles: Spike, Stress, Soak Patterns →](https://docs.volt-test.com/docs/load-profiles) - [PHP Load Testing Guide →](/blog/php-load-testing) - [Symfony Load Testing: FrankenPHP vs PHP-FPM →](/blog/symfony-load-testing) - [How to Stress Test PHP Applications: Bottlenecks and Breaking Points →](/blog/php-stress-testing-tool) --- GitHub: [volt-test/php-sdk](https://github.com/volt-test/php-sdk) | [volt-test/laravel-performance-testing](https://github.com/volt-test/laravel-performance-testing) Follow updates on X: [@VT_Developers](https://x.com/VT_Developers) --- ### PHP Load Testing: How to Load Test PHP Applications URL: https://volt-test.com/blog/php-load-testing Published: 2026-06-19 Keywords: php load testing, php load testing tools, load testing php, php performance testing, php performance testing tool, performance testing php, php load test # How to Load Test PHP Applications PHP powers roughly [77% of all websites with a known server-side language](https://w3techs.com/technologies/details/pl-php) — 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. TL;DR: PHP load testing verifies your application handles real traffic levels — not just your laptop's dev server. VoltTest lets you write load tests in PHP, install via Composer, and scale from local runs to 10M+ concurrent users in the cloud. This guide covers tool selection, comparison, and step-by-step implementation for both plain PHP and Laravel. ![PHP load testing results showing 99.94% success rate, 540 requests per second, and 128ms P95 latency](/img/php-load-testing-og.png) ## 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 (we break these down in detail in our [performance testing types](/blog/performance-testing-types) guide): - **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](/blog/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](/blog/laravel-load-testing-with-phpunit) makes this straightforward. ## What Should You 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. ## Which PHP Load Testing Tool Should You Use? Here's an honest comparison of the most common options: | Feature | VoltTest | Apache JMeter | k6 (Grafana) | Locust | Apache Bench | |---|---|---|---|---|---| | Write tests in | PHP | Java / XML GUI | JavaScript | Python | CLI flags | | Install via | Composer | Download JVM app | Install binary | pip install | Pre-installed (most OS) | | Laravel integration | Native package | None | None | None | None | | Multi-step scenarios | Yes | Yes | Yes | Yes | No | | Concurrent VUs (local) | Thousands | Hundreds | Thousands | Hundreds | Limited | | Cloud scaling | Built-in | DIY infrastructure | Grafana Cloud | DIY infrastructure | N/A | | P95/P99 metrics | Yes | Yes | Yes | Yes | No | | Data-driven (CSV) | Yes | Yes | Yes | Yes | No | | CI/CD integration | PHPUnit | Jenkins plugin | CLI | CLI | CLI | | Protocols supported | HTTP only | HTTP, JDBC, JMS, FTP, LDAP, TCP | HTTP, WebSocket, gRPC, browser | HTTP, plus any Python client | HTTP only | | Per-step assertions | Status code only | Full assertion library | Arbitrary JS checks | Arbitrary Python | None | | Maturity / ecosystem | New (2025) | Since 1998, large plugin ecosystem | Large, Grafana-backed | Mature Python project | Ships with Apache | - **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](https://jmeter.apache.org/)** 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. We break the trade-offs down in [VoltTest vs JMeter](/compare/volttest-vs-jmeter). - **[k6](https://grafana.com/docs/k6/latest/)** is modern and well-documented, but you write tests in JavaScript — a context switch for a PHP team. See [how the two compare on a PHP stack](/compare/volttest-vs-k6). - **[Locust](https://locust.io/)** 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. The trade-off is deliberate narrowness: it speaks HTTP only, its per-step assertion is the status code, and it's the youngest tool here. If you need to load test a message queue or a database directly, or assert on response bodies step by step, reach for JMeter or k6. ## How Do You Load Test 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 ```bash composer require volt-test/php-sdk ``` ### Basic Load Test — Single Endpoint Start simple: 50 concurrent users hitting an API endpoint for 30 seconds. ```php title="load-test.php" 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: ```bash 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 — the same shape as a [storefront checkout rehearsal](/solutions/ecommerce-load-testing): ```php title="checkout-flow-test.php" 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: ```csv title="users.csv" email,password user1@example.com,password123 user2@example.com,password123 user3@example.com,password123 ``` ## How Do You Load Test a Laravel Application? If you're on Symfony, the [Symfony load testing guide](/blog/symfony-load-testing) walks through CSRF forms, a JWT API and a Doctrine checkout on FrankenPHP versus php-fpm, with measured results. If you're on Laravel, the [dedicated Laravel package](/solutions/laravel-load-testing) adds Artisan commands, automatic CSRF handling, and PHPUnit integration on top of the core SDK. (See also: [Effortless Laravel Performance Testing with VoltTest](/blog/effortless-laravel-performance-testing-with-volt-test-php-sdk) for a deeper walkthrough.) ### Install ```bash 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: ```bash 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: ```bash php artisan volttest:make CheckoutLoadTest ``` Define the scenario with CSRF token handling and data sources: ```php title="app/VoltTests/CheckoutLoadTest.php" 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: ```bash 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: ```php title="tests/Performance/CheckoutLoadTest.php" runVoltTest(new VoltCheckout, [ 'virtual_users' => 50, ]); $this->assertVTSuccessful($result, 99); $this->assertVTP95ResponseTime($result, 200); $this->assertVTP99ResponseTime($result, 500); $this->assertVTErrorRate($result, 1); } } ``` ```bash 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: ```csv title="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: ```php $scenario->setDataSourceConfiguration( new DataSourceConfiguration(__DIR__ . '/users.csv', 'unique', true) ); ``` In Laravel, use the shorthand: ```php $scenario = $manager->scenario('User Flow') ->dataSource('users.csv', 'unique'); ``` The iteration mode controls how rows are assigned to virtual users: | Mode | Behavior | |---|---| | `unique` | Each VU gets a different row. VUs > rows causes an error. | | `sequential` | VUs cycle through rows in order, wrapping around. | | `random` | Each 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: ```text 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. ## What Are the Most 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: ```bash php artisan volttest:run CheckoutLoadTest --users=5000 --duration=5m --cloud ``` Or in plain PHP, enable cloud mode with your API key: ```php $test->cloud('vt_YOUR_API_KEY'); ``` Try VoltTest Cloud Free: Run distributed load tests at scale without managing infrastructure — 500 VUs free, no credit card. ## 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 - [PHP Stress Testing Tool: How to Stress Test PHP & Laravel Apps →](/blog/php-stress-testing-tool) - [Stress Testing Laravel Applications with VoltTest (Web UI Flow) →](/blog/stress-testing-laravel-with-volt-test-web-ui) - [Load Testing Laravel with PHPUnit →](/blog/laravel-load-testing-with-phpunit) - [Symfony Load Testing: FrankenPHP vs PHP-FPM →](/blog/symfony-load-testing) - [Introducing VoltTest: The PHP-Native Load Testing SDK →](/blog/introducing-volt-test-php-load-testing) - [VoltTest Cloud Closed Beta: How to Get Access →](/blog/volt-test-cloud-closed-beta-open) - [VoltTest PHP SDK Documentation →](https://docs.volt-test.com/docs/introduction) --- ⭐ **Star the repository on GitHub:** [volt-test/php-sdk](https://github.com/volt-test/php-sdk) 💬 **Follow updates on X:** [@VoltTest](https://x.com/VT_Developers) --- --- ### VoltTest Cloud Closed Beta Is Now Open: How to Get Access URL: https://volt-test.com/blog/volt-test-cloud-closed-beta-open Published: 2026-06-12 Keywords: volt-test cloud, closed beta, cloud load testing, php load testing, laravel load testing, distributed load testing, load testing platform # VoltTest Cloud Closed Beta Is Now Open We're opening up access to **VoltTest Cloud** — the managed platform for running large-scale load tests without provisioning a single server. Until now, cloud mode has been limited to a small group of early testers. Starting today, anyone can join the waitlist, and we're approving access in waves. This post covers what VoltTest Cloud is, what you get in the closed beta, and exactly how the access process works from waitlist to your first cloud test run. TL;DR: VoltTest Cloud runs your PHP load tests on managed infrastructure — scale from hundreds to millions of concurrent users across regions without managing servers. The closed beta is now accepting waitlist signups. ![VoltTest Cloud Closed Beta Is Now Open](/img/volt-test-cloud-closed-beta-og.png) ## What Is VoltTest Cloud? If you've used the [VoltTest PHP SDK](https://github.com/volt-test/php-sdk), you know the workflow: define scenarios in plain PHP, run them on the high-performance Go engine, and read real percentile metrics. That works great from your own machine — up to a point. Generating serious load from a laptop or a single CI runner hits hard limits: open file descriptors, bandwidth, CPU. VoltTest Cloud removes that ceiling. Your test definition stays exactly the same — one line switches execution to managed infrastructure: ```php target('https://staging.example.com'); $test->cloud('vt_your_api_key'); // ← runs on VoltTest Cloud instead of locally $test->setVirtualUsers(500); $test->setDuration('5m'); ``` Using the [Laravel package](https://docs.volt-test.com/docs/laravel/laravel-quick-start)? It's just as simple — add your API key to `.env` and pass a flag: ```bash php artisan volttest:run CheckoutTest --cloud ``` Or enable it globally with `VOLTTEST_CLOUD_ENABLED=true` in your `config/volttest.php` — the [Cloud Execution docs](https://docs.volt-test.com/docs/laravel/laravel-cloud-mode) cover all the options. Either way, the test executes on dedicated cloud instances, and results — requests per second, P95/P99 latency, error breakdowns, time-series charts — land in your VoltTest dashboard, stored and comparable across runs. ![A completed test run in the VoltTest Cloud dashboard: 473,948 requests at 7,116 peak RPS, with automatic insights flagging P99 outlier spikes and a full latency distribution](/img/volt-test-cloud-run-overview.png) This run is a good example of why you load test in the first place: averages look healthy (35ms), but VoltTest's insights immediately flag that P99 is 126× higher than P50 — a tail-latency problem a quick manual check would never catch. ## What's Included in the Closed Beta Every beta account starts on the free plan: | | Free (Beta) | |---|---| | Max virtual users per test | **500** | | Max test duration | **10 minutes** | Everything else comes with the account: the full dashboard with metrics and run history, run comparison (up to 5 runs side-by-side), and the PHP SDK & Laravel integrations. 500 VUs for 10 minutes is enough to find real bottlenecks in most staging environments — connection pool exhaustion, N+1 queries under concurrency, and tail-latency spikes all show up well before that. We're finalizing paid tiers with higher limits during the beta — early users get a say in how they're shaped. Serious about scale?: If the free limits are holding you back, reach out at [hello@volt-test.com](mailto:hello@volt-test.com) — we're happy to upgrade serious beta testers to a higher package while pricing is being finalized. Tell us what you're testing and the load you need. ## How to Get Access We're keeping the beta *closed* — access is granted in waves — but the door is now open for everyone to request it. Here's the full journey: ### 1. Create Your Account Sign up at [volt-test.com/register](https://volt-test.com/register) — registering is how you join the waitlist. It's free, with no credit card and no commitment, and your account is already set up for the moment you're approved. Try VoltTest Cloud Free: ### 2. Verify Your Email Right after signing up, we'll send a 6-digit verification code to your inbox. Enter it and your account is active — you'll land on a *"You're on the waitlist"* screen while your access is being reviewed. ### 3. Get Approved We review the waitlist and approve access in waves. When it's your turn, you'll get an email — and if you're signed in, the waitlist screen notices your approval and takes you straight to the dashboard, no re-registering or refreshing needed. We're prioritizing teams actively load testing PHP and Laravel applications — if that's you, an email to [hello@volt-test.com](mailto:hello@volt-test.com) with a line about your use case helps us bump you up the queue. ### 4. Run Your First Cloud Test First, grab an API key: from the dashboard, open **Settings → API Keys**, click **Create API Key**, and give it a name (you can set an expiration, or leave it permanent). Copy the generated key — it starts with `vt_` and is shown only once, so store it somewhere safe. Then point your existing test at the cloud. With the PHP SDK: ```php $test->cloud('vt_your_api_key'); ``` With the Laravel package, add `VOLTTEST_API_KEY=vt_your_api_key` to `.env` and run: ```bash php artisan volttest:run YourTest --cloud ``` Run it the same way you always do — the SDK handles the rest, and your results appear in the dashboard in real time. The [Cloud Mode guide](https://docs.volt-test.com/docs/cloud-mode) (or [Cloud Execution for Laravel](https://docs.volt-test.com/docs/laravel/laravel-cloud-mode)) walks through the full setup. ## Why a Closed Beta? We'd rather grow deliberately than fall over publicly. A load testing platform has an unusual property: our users' job is to generate enormous amounts of traffic. Opening access in waves lets us watch capacity, harden the orchestration layer, and talk to every early user — your feedback directly shapes what we build next. If something breaks, confuses you, or is missing, we want to hear about it. Beta users get a direct line to us at [hello@volt-test.com](mailto:hello@volt-test.com). ## What's Next During the beta we're focused on: - **Paid tiers** — higher VU counts, longer tests, and parallel runs; pricing is being finalized now and will be announced during the beta - **Scheduled runs** — recurring cloud tests on a schedule, on top of the API-key-triggered runs you can already wire into CI today - **Regression detection** — automated baselines and pass/fail thresholds, building on the run comparison already in the dashboard ## Get Started Today 1. **Join the waitlist** — create your account at [volt-test.com/register](https://volt-test.com/register) 2. **Try it locally while you wait** — the [PHP SDK Getting Started guide](https://docs.volt-test.com/docs/getting-started) or the [Laravel Quick Start](https://docs.volt-test.com/docs/laravel/laravel-quick-start) takes about five minutes 3. **Read the cloud docs** — so you're ready to go the moment your invite lands: [Cloud Mode →](https://docs.volt-test.com/docs/cloud-mode) or [Cloud Execution for Laravel →](https://docs.volt-test.com/docs/laravel/laravel-cloud-mode) ## Learn More **On the platform** - [What VoltTest Cloud does →](/features) - [Load testing use cases →](/solutions) **Cloud documentation** - [Cloud Mode (PHP SDK) →](https://docs.volt-test.com/docs/cloud-mode) - [Cloud Examples →](https://docs.volt-test.com/docs/Examples/cloud-examples) - [Cloud Execution (Laravel Package) →](https://docs.volt-test.com/docs/laravel/laravel-cloud-mode) **PHP SDK** - [Installation →](https://docs.volt-test.com/docs/installation) - [Getting Started →](https://docs.volt-test.com/docs/getting-started) **Laravel package** - [Installation →](https://docs.volt-test.com/docs/laravel/laravel-installation) - [Quick Start →](https://docs.volt-test.com/docs/laravel/laravel-quick-start) - [Artisan Commands →](https://docs.volt-test.com/docs/laravel/laravel-cli-commands) **From the blog** - [How to Stress Test PHP and Laravel Applications →](/blog/php-stress-testing-tool) - [Performance Testing Types Explained →](/blog/performance-testing-types) --- ⭐ **Star the repository on GitHub:** [volt-test/php-sdk](https://github.com/volt-test/php-sdk) 💬 **Follow updates on X:** [@VoltTest](https://x.com/VT_Developers) --- --- ### PHP Stress Test Tool: Where PHP & Laravel Break URL: https://volt-test.com/blog/php-stress-testing-tool Published: 2026-06-11 Keywords: php stress testing, how to stress test php, php stress test tool, php performance testing, php load testing tools, php performance testing tool, php stress test, php bottlenecks under load, php fpm max children, php session locking, load testing php # PHP Stress Test Tool: How to Stress Test PHP and Laravel Choosing a **PHP stress test tool** usually means a painful trade-off: the popular load testing tools all live outside PHP, so you end up writing scenarios in JavaScript, Java, or XML instead of the language your app is built in. This guide takes a different approach. We'll cover what stress testing actually means for a PHP application, the bottlenecks that PHP apps specifically hit under load, what to look for in a tool, how the main options compare, and how to run your first stress test on plain PHP or Laravel in just a few minutes — without leaving your Composer workflow. TL;DR: Stress testing pushes your PHP application past its expected limits to find where it breaks. In PHP that's usually one of five things: FPM worker saturation, database connection limits or N+1 amplification, session locking, cache stampede, or a queue backlog. VoltTest lets you write the tests that find them in PHP itself, and run them locally or at cloud scale. ![PHP Stress Testing Tool](/img/php-stress-testing-tool-og.png) ## What Is Stress Testing in PHP? Stress testing pushes your application beyond its normal operating limits to find the point where it breaks — and to see *how* it breaks. It answers questions a single request never can: How many concurrent users can your API handle before response times spike? Does the database connection pool exhaust under load? Does the app fail gracefully or fall over? VoltTest is a stress testing tool that lets PHP and Laravel teams answer those questions in PHP itself. It's easy to confuse the different flavors of performance testing. The difference isn't the tool — it's the question you're asking: | | Load testing | Stress testing | Spike testing | |---|---|---|---| | **Question** | Does it hold up at expected traffic? | Where does it break, and how? | Does it survive a sudden surge? | | **Load profile** | Ramp to target, then hold | Keep climbing past target | Jump straight to a multiple of target | | **Pass condition** | P95 under budget, errors near zero | You found the limit and it degraded gracefully | It recovers once the surge passes | | **Typical trigger** | Pre-release regression check | Capacity planning before a campaign | Black Friday, a launch, a viral post | They overlap heavily and most tools (including VoltTest) handle all three. For a full breakdown of each type, see [Performance Testing Types](/blog/performance-testing-types). For the rest of this article we'll use "stress testing" loosely to mean putting your PHP app under realistic concurrent load. ### The Knee Matters More Than the Breaking Point The **knee** is the virtual-user count where P95 latency stops rising gently and starts bending upward while throughput flattens. It is your application's real capacity, and it arrives well before the first error. Most teams stress test looking for the moment the application starts returning errors. That number is the *least* useful thing the test produces, because you will never run production anywhere near it. Below the knee, adding users adds throughput. Above it, adding users only adds queueing: the same requests per second, served more slowly to more people. The breaking point — 5xx responses, timeouts, connection resets — usually arrives well *after* the knee, and by then your users have been having a bad time for a while. So a useful PHP stress test produces three numbers, not one: - **The knee** — where latency starts bending. This is your real capacity. - **The breaking point** — where errors start. The gap between it and the knee is your headroom. - **Recovery time** — how long the system takes to return to baseline once load stops. An app that needs ten minutes to drain a queue has a very different failure mode from one that recovers in ten seconds. ## PHP Stress Test Tool Comparison Here's an honest look at the most common options for stress testing a PHP application: | Tool | Write tests in | PHP-native | Laravel-native | Multi-step scenarios | Best for | |---|---|---|---|---|---| | **VoltTest** | PHP | Yes | Yes | Yes | PHP/Laravel teams who want native tests on a fast engine | | [k6](https://k6.io/) | JavaScript | No | No | Yes | JS-heavy teams comfortable scripting in JS | | [JMeter](https://jmeter.apache.org/) | XML / GUI | No | No | Yes | Protocol breadth and complex enterprise plans | | [Locust](https://locust.io/) | Python | No | No | Yes | Python teams | | [Apache Bench](https://httpd.apache.org/docs/current/programs/ab.html) (`ab`) | CLI flags | No | No | No | Quick one-off single-endpoint checks | There's no single "best" tool — it depends on your stack: - **k6** is excellent if your team already lives in JavaScript. ([VoltTest vs k6](/compare/volttest-vs-k6) covers where each one fits.) - **JMeter** is the veteran for protocol breadth and large enterprise test plans, at the cost of a heavier, GUI-driven workflow. ([VoltTest vs JMeter](/compare/volttest-vs-jmeter).) - **Apache Bench** is perfect for a quick `ab -n 1000 -c 50` sanity check on a single URL, but can't model real user journeys. - **VoltTest** is built specifically for PHP and Laravel developers: you write tests in PHP, install with Composer, and a Go engine handles the actual load generation — so you get PHP's ergonomics with Go's concurrency. It's the only option here with a [first-party Laravel package](/solutions/laravel-load-testing) — and on Laravel it gives you the same one-line speed as `ab` (`php artisan volttest:run `) *and* full multi-step scenarios when you need them. What it gives up for that focus: HTTP only (no JDBC, JMS or gRPC workloads), status-code-only per-step assertions, and the shortest track record of anything in this table. JMeter and k6 are the better tools when you need protocol breadth or richer assertions. ### What to Look For in a PHP Stress Test Tool Not every load testing tool fits a PHP team. Before picking one, check it against these criteria: - **PHP- and Composer-native** — 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 log in, browse, add to cart, and check out. The tool should chain requests and pass data (tokens, IDs, CSRF) between steps. - **Ramp-up and stages** — instantly hitting your app with 1,000 users isn't realistic. You want ramp-up, hold, spike, and ramp-down profiles. - **Percentile metrics** — averages hide problems. You need P95 and P99 latency, requests per second, and success/error rates. - **Session and cookie handling** — without it you only ever test anonymous traffic, and PHP's session lock (see the bottlenecks below) never shows up. - **CI/CD integration** — stress tests are most valuable when they run automatically and catch regressions before they ship. - **Assertions** — the ability to fail a build when, say, P95 exceeds 150ms. - **Scale** — can it generate enough concurrent virtual users to actually stress your infrastructure? The first point is where most tools fall short for PHP developers, and it is the reason VoltTest leads the table above. Whichever tool you pick, it has to find the same five bottlenecks. Those come next. ## Common PHP Bottlenecks Under Stress Under load, a PHP application almost never fails where you expect. The code you spent an afternoon optimizing is rarely the constraint — the constraint is usually a *fixed-size resource* underneath it: a worker pool, a connection limit, a lock, a queue. Stress testing is how you find out which one you run out of first. These five account for most PHP failures under load. For each: what actually breaks, the signature it leaves in your metrics, and how to reproduce it deliberately. ### 1. PHP-FPM Worker Saturation PHP-FPM serves each request from a pool of worker processes capped by [`pm.max_children`](https://www.php.net/manual/en/install.fpm.configuration.php). When every worker is busy, new requests don't fail — they queue in the socket backlog. Then they wait. The arithmetic is unforgiving. With `pm.max_children = 20` and an average request taking 100ms, your ceiling is roughly 200 requests per second no matter how many virtual users you throw at it. This is the first wall most PHP apps hit, and it's routinely misdiagnosed as a slow database. **The signature:** throughput plateaus at a hard ceiling while latency climbs in a straight line. Double the virtual users and RPS doesn't move, but P95 roughly doubles. Push further and nginx starts returning `502 Bad Gateway` or `504 Gateway Timeout`. **Reproduce it** by ramping past the ceiling in stages and watching where the RPS line goes flat: ```php title="breakpoint-test.php" stage('1m', 50) // warm up ->stage('2m', 200) // climb ->stage('2m', 500) // past the expected ceiling ->stage('1m', 0); // ramp down and watch recovery $scenario = $voltTest->scenario('Product Listing'); $scenario->step('Browse products') ->get('https://staging.example.com/products') ->validateStatus('Products OK', 200); $voltTest->run(true); ``` **The fix is usually configuration, not code:** raise `pm.max_children` (bounded by available RAM ÷ average process size — over-provisioning here just moves the failure to swap), or shorten the time each worker is held. Shortening it generally means fixing one of the next four. ### 2. Database Connections and N+1 Amplification Two distinct database problems show up under stress, and they look different. The first is **connection exhaustion**. MySQL's `max_connections` and your FPM worker count are independent numbers that have to agree. Two hundred FPM workers each opening a connection against a `max_connections = 151` server produces [`SQLSTATE[HY000] [1040] Too many connections`](https://dev.mysql.com/doc/refman/8.4/en/too-many-connections.html) — an application-level 500, not a slow response. The second is **N+1 amplification**, and it's the more common one. A page that issues 30 queries at one virtual user issues 3,000 at a hundred. The query that took 2ms in isolation now waits behind ninety-nine copies of itself. Nothing in your single-request profiling predicts this, because the cost isn't in the query — it's in the contention. **The signature:** latency climbs with a *long tail*. P99 degrades far faster than P95 (a 5× gap between them is a strong hint), and the database host shows high CPU or lock waits while your PHP servers sit relatively idle. **Reproduce it** with realistic data variation, not one URL repeated. Hitting `/products/1` ten thousand times measures your query cache; hitting ten thousand different product IDs measures your database. VoltTest reads a CSV data source for this, in `sequential`, `random`, or `unique` mode, and interpolates the column into the request: ```php use VoltTest\DataSourceConfiguration; $scenario->setDataSourceConfiguration( new DataSourceConfiguration(__DIR__ . '/product_ids.csv', 'random', true) ); $scenario->step('Product detail') ->get('https://staging.example.com/products/${product_id}') ->validateStatus('Product OK', 200); ``` ### 3. Session Locking Session locking is where PHP's default file-based session handler holds an **exclusive lock** on the session file for the whole request, so a second request carrying the same session cookie blocks until the first finishes and releases the lock. It is the PHP-specific bottleneck that surprises people most, because it doesn't exist in most other runtimes. A modern page firing six AJAX requests at once executes them strictly one after another. **The signature:** latency scales with *requests per session*, not with virtual users. CPU sits idle on every tier while response times climb. The tell-tale comparison: the same steps run dramatically faster when the scenario doesn't carry cookies. That last point is also the reason most stress tests miss this entirely. If every virtual user is cookie-less and anonymous, there's no shared session and no lock to contend for — the test passes cleanly and production still stalls. Session persistence is opt-in: ```php $scenario = $voltTest->scenario('Authenticated Browsing') ->autoHandleCookies(); // without this, no session, no lock contention ``` **The fix:** move sessions to a non-locking handler (Redis with locking disabled), or call [`session_write_close()`](https://www.php.net/manual/en/function.session-write-close.php) as soon as the request is done writing to the session — which releases the lock while the rest of the request runs. ### 4. Cache Misses and Stampede Your application is fast because the cache is warm. Stress testing a warm cache measures the cache, not the application. The dangerous moment is the **stampede**: a popular key expires, and every in-flight request misses simultaneously. All of them try to regenerate the same expensive value at once, and a query that normally runs once per five minutes runs five hundred times in one second. Under enough concurrency this alone can take down the database. **The signature:** steady-state numbers look excellent, but a spike profile — or the first sixty seconds after a deploy or `cache:clear` — shows a wall of latency followed by recovery. If your P95 is fine and your P99.9 is catastrophic, suspect regeneration. **Reproduce it** by flushing the cache immediately before a spike stage, so the surge lands on a cold cache the way a deploy does: ```php $voltTest->stage('10s', 500) // near-instant surge onto a cold cache ->stage('2m', 500) // hold, and watch whether it recovers ->stage('30s', 0); ``` **The fix:** single-flight regeneration (a lock so one request rebuilds while the others wait or serve stale), and staggered TTLs so keys written together don't expire together. ### 5. Queue Backlogs Queues are the bottleneck a stress test is most likely to declare healthy while the feature is broken. An endpoint that dispatches a job returns in 15ms whether the worker pool is idle or forty thousand jobs behind. Your HTTP metrics stay green — 100% success, low P95 — while order confirmations go out three hours late. The load test passed; the system failed. **The signature:** there isn't one in the HTTP metrics. That's the point. You have to watch queue depth and job latency *alongside* the run, and treat "backlog still draining ten minutes after the test ended" as a failure even when every request succeeded. On Laravel, that means watching Horizon (or `queue:monitor`) during the run and checking `failed_jobs` afterwards. Worker throughput is a fixed capacity like any other: if the endpoint can dispatch 500 jobs/second and your workers process 50, you have a 10× deficit that only shows up as a growing number somewhere else. ### Telling Them Apart When a stress test goes bad, the combination of symptoms usually narrows it to one suspect: | Symptom | Most likely cause | |---|---| | RPS flat, latency linear, PHP hosts at 100% CPU | FPM workers or CPU-bound code | | RPS flat, latency linear, everything *idle* | Session locking, or a blocking external call | | P99 far worse than P95, DB host busy | N+1 amplification or lock contention | | Fine in steady state, terrible for the first minute | Cold cache / stampede | | Every HTTP metric green, work not actually done | Queue backlog | | 500s with SQLSTATE 1040 | Connection limit mismatch with FPM workers | The next sections show exactly how to run these tests with VoltTest — from a stress test in plain PHP, to a full Laravel scenario, to a one-line CLI test you can fire off like `ab`. ## Stress Testing Plain PHP with VoltTest VoltTest's core SDK is framework-agnostic — it works with any PHP application, whether that's Symfony, WordPress, or raw PHP. Install it with Composer: ```bash composer require volt-test/php-sdk ``` Then define a scenario and run it. Here's a minimal stress test that hits your homepage: ```php title="stress-test.php" setVirtualUsers(50) ->setRampUp('10s') ->setDuration('1m'); // Define a scenario with one or more steps $scenario = $voltTest->scenario('Browse Homepage'); $scenario->step('Load homepage') ->get('https://your-app.test/') ->validateStatus('Homepage OK', 200); // Run the test — passing true streams the live report to your console $voltTest->run(true); ``` Run it like any PHP script: ```bash php stress-test.php ``` Because the heavy lifting happens in VoltTest's Go engine, a single machine can drive far more concurrent virtual users than a pure-PHP runner could — while you keep writing tests in the language you already use. That first script uses a *constant* load profile: a fixed number of virtual users, held for a fixed duration. It answers "does 50 users hurt?" — a load test question. To find the knee you need the load to keep climbing, which means stages: ```php title="find-the-knee.php" stage('30s', 25) // baseline: what does "healthy" look like? ->stage('1m', 100) ->stage('1m', 250) ->stage('1m', 500) ->stage('30s', 0); // ramp down — recovery is data too $scenario = $voltTest->scenario('Browse Homepage'); $scenario->step('Load homepage') ->get('https://staging.example.com/') ->validateStatus('Homepage OK', 200); $voltTest->run(true); ``` Start the ladder at a load you're confident is healthy. Without that baseline you get a breaking point with nothing to compare it against, and no way to tell whether P95 at 250 VUs is a problem or just Tuesday. ## Stress Testing a Laravel Application If you're on Laravel, the dedicated package builds on the core SDK and adds Laravel-specific conveniences: Artisan commands, automatic route discovery, and CSRF/cookie handling. Install it as a dev dependency: ```bash composer require volt-test/laravel-performance-testing --dev php artisan vendor:publish --tag=volttest-config ``` Scaffold a test with Artisan: ```bash php artisan volttest:make LoginTest ``` Then define the scenario. Note `extractCsrfToken()` — the package pulls Laravel's CSRF token out of the response automatically, so you don't have to: ```php title="app/VoltTests/LoginTest.php" target('http://localhost:8000'); $scenario = $manager->scenario('Login Flow'); $scenario->step('Get Login Page') ->get('/login') ->expectStatus(200) ->extractCsrfToken(); $scenario->step('Submit Login') ->post('/login', [ 'email' => 'user@example.com', 'password' => 'password', '_token' => '${csrf_token}', ]) ->expectStatus(302); } } ``` The Laravel package also supports **stages** (ramp-up, hold, spike, ramp-down), **CSV data sources** for driving tests with realistic data, and a **PHPUnit integration** so your stress tests run inside your existing test suite with assertions like `assertVTP95ResponseTime()`. Everything in the bottlenecks section above applies to Laravel too, with a few framework-specific wrinkles: Eloquent makes N+1 amplification easy to write by accident, the default `file` session driver carries the same lock as plain PHP, and queue backlogs are usually watched through Horizon. Three guides cover the Laravel workflow in depth: - [How to Load Test Laravel Applications](/blog/laravel-load-testing) — the full pillar guide: CSRF and Sanctum auth flows, queues, CI/CD, and interpreting Laravel-specific results. - [Stress Testing Laravel Applications with VoltTest (Web UI Flow)](/blog/stress-testing-laravel-with-volt-test-web-ui) — a full registration-to-dashboard scenario. - [Load Testing Laravel Applications with PHPUnit and VoltTest](/blog/laravel-load-testing-with-phpunit) — run performance assertions inside PHPUnit and catch regressions in CI. ## Quick One-Off Stress Tests from the Command Line Sometimes you don't want to write a test class at all — you just want to hammer one endpoint and see if it holds up, the way you would with `ab -n 1000 -c 50`. The Laravel package's `volttest:run` command does exactly that, with none of `ab`'s limitations. Stress test a single URL with 50 virtual users for one minute — the command auto-detects that the argument is a URL, so there's nothing else to configure: ```bash php artisan volttest:run https://api.example.com/health --users=50 --duration=1m ``` You can also stress test authenticated `POST` endpoints with a request body inline: ```bash php artisan volttest:run https://example.com/api/login \ --users=100 \ --method=POST \ --body='{"email":"test@example.com","password":"secret"}' ``` Useful flags (run `php artisan volttest:run --help` for the full list): | Flag | Purpose | |---|---| | `--users=` | Number of virtual users | | `--duration=` | How long to run (e.g. `30s`, `1m`, `5m`) | | `--method=` | HTTP method — `GET`, `POST`, `PUT`, `DELETE` | | `--body=` | Request body for `POST`/`PUT` | | `--headers=` | JSON string of request headers | | `--code-status=` | Expected HTTP status code (default `200`) | | `--stage=` | Load stages as `duration:target`, e.g. `--stage=1m:50 --stage=5m:100 --stage=1m:0` | | `--cloud` | Run the test on VoltTest Cloud | | `--stream` | Stream live output to the console | This gives you the one-liner speed of Apache Bench but with real virtual-user behavior, percentile metrics, ramp-up stages, and a path to cloud-scale execution — all from a single Artisan command. ## Reading Your Stress Test Results A stress test is only useful if you can interpret the report. VoltTest output looks like this: ```text Performance Report: Homepage Stress Test ---------------------------------------------------------------------- Total Requests: 5000 Success Rate: 99.94% Requests/Sec (RPS): 346.19 Avg Latency: 74.24ms P95 Latency: 128.71ms P99 Latency: 210.05ms ---------------------------------------------------------------------- ``` What to focus on: - **Success Rate** — anything below ~99% under load usually means the app is dropping requests or erroring out. - **Requests/Sec (RPS)** — your real-world throughput ceiling. - **Avg Latency** — useful, but it *hides* outliers. Don't ship on the average alone. - **P95 / P99 Latency** — the latency 95% and 99% of users experience. This is where real problems surface: an average of 74ms with a P99 of 2,000ms means 1 in 100 users is having a terrible time. ### The Saturation Signature (Illustrative Numbers, Not a Measured Run) A single run gives you a snapshot. Running the same scenario at increasing virtual-user counts and lining the numbers up gives you the shape — which is what you actually came for: | VUs | RPS | P95 | P99 | Errors | |---|---|---|---|---| | 50 | 340 | 129ms | 210ms | 0% | | 100 | 670 | 141ms | 233ms | 0% | | 200 | 690 | 402ms | 1,180ms | 0% | | 400 | 695 | 1,340ms | 4,900ms | 2.1% | Throughput stops growing between 100 and 200 virtual users. That's the knee, and this system's real capacity is around 100 concurrent users — not 400. Everything past the knee is queueing: the same ~690 requests per second delivered to twice as many people at three times the latency. The errors at 400 VUs are the breaking point, but the user-visible damage started 300 virtual users earlier, while the success rate still read 100%. Note also that P99 degrades much faster than P95 across those rows — the long-tail pattern from the database section above. On these numbers: The figures above are illustrative. The *shape* is what transfers between applications — a throughput plateau with climbing latency — not the specific values, which depend entirely on your hardware, your code, and your endpoint. ### Reading the Errors When requests do start failing, the status code narrows the search considerably: | What you see | Where to look first | |---|---| | `502 Bad Gateway` | PHP-FPM pool exhausted, or a worker died — check the FPM error log | | `504 Gateway Timeout` | A request outran nginx's [`fastcgi_read_timeout`](https://nginx.org/en/docs/http/ngx_http_fastcgi_module.html#fastcgi_read_timeout) — usually a slow query or a blocking external API call | | `500` with `SQLSTATE` | Database connection limit, lock wait timeout, or a deadlock | | Connection reset / refused | You hit an OS or load-balancer limit before reaching PHP — `somaxconn`, file descriptors, ephemeral ports | | Timeouts reported only by the client | Frequently the load generator itself, not the target | That last row is worth taking seriously. A load generator saturating its own CPU reports latency that belongs to the test machine. Check the generator's resource usage before you go looking for the bottleneck in your application — VoltTest's Go engine keeps per-VU overhead low precisely so this happens later, but "later" is not "never". ## Common PHP Stress Testing Mistakes A few mistakes make stress test results misleading: - **Testing localhost** — your laptop is not production. Network, CPU, and database differ wildly. Test against a staging environment that mirrors production. - **No ramp-up** — slamming the app with all users at once measures a thundering herd, not real traffic. Use stages to ramp up gradually. - **Ignoring P99** — averages look fine while a slice of users suffer. Always read the tail latencies. - **Stress testing production unannounced** — you can take down a live system. Coordinate with your team, and prefer staging or a clearly-marked maintenance window. - **No baseline run** — a breaking point on its own is a number with nothing to compare it to. Establish the healthy P95 at low load first, then climb. - **Hammering one endpoint** — a single URL rarely exercises the resource you'll actually run out of. Real traffic mixes cheap and expensive routes and shares a session, and the interaction between them is where the bottleneck usually lives. - **Only ever testing a warm cache** — if you never test a cold start, you never test the scenario that follows every deploy. ## Conclusion Stress testing tells you how your PHP application behaves under pressure *before* your users find out the hard way. Most of the value isn't in the breaking point — it's in learning which of the five bottlenecks you hit first, because that's the one worth spending a sprint on. The right tool removes the friction: with VoltTest you write tests in PHP, install via Composer, run them against plain PHP or Laravel, and read real percentile metrics — all on a high-performance Go engine. Start with a baseline run on staging, add stages until throughput plateaus, note where the knee lands, and wire the test into CI so the next regression shows up in a pull request instead of in production. Running stress tests from one machine works well up to a point. To simulate hundreds of thousands of concurrent users from multiple regions, you need distributed infrastructure — and that's what VoltTest Cloud provides, without you managing a single server. Try VoltTest Cloud Free: Run distributed load tests at scale without managing infrastructure — 500 VUs free, no credit card. ## Learn More - [PHP Load Testing: How to Load Test PHP Applications →](/blog/php-load-testing) - [How to Load Test Laravel Applications →](/blog/laravel-load-testing) - [Performance Testing Types Explained →](/blog/performance-testing-types) - [Stress Testing Laravel with the VoltTest Web UI →](/blog/stress-testing-laravel-with-volt-test-web-ui) - [Load Testing Laravel with PHPUnit →](/blog/laravel-load-testing-with-phpunit) - [Load Testing PHP APIs →](/solutions/php-api-testing) - [Effortless Laravel Performance Testing with VoltTest PHP SDK →](/blog/effortless-laravel-performance-testing-with-volt-test-php-sdk) --- ⭐ **Star the repository on GitHub:** [volt-test/php-sdk](https://github.com/volt-test/php-sdk) 💬 **Follow updates on X:** [@VoltTest](https://x.com/VT_Developers) --- --- ### Load Testing Laravel Applications with PHPUnit and VoltTest URL: https://volt-test.com/blog/laravel-load-testing-with-phpunit Published: 2025-11-29 Keywords: laravel performance testing, laravel load testing, phpunit load test, volt-test, laravel testing tools # Introducing Load Testing Inside PHPUnit With VoltTest 1.2.0, you can now run load tests directly inside PHPUnit — no external scripts or configuration required. VoltTest was built to make load testing simple and native for PHP developers, and this new release takes it one step further. You can now test your Laravel APIs under load using the same PHPUnit environment you already use for your feature and unit tests. TL;DR: VoltTest 1.2.0 lets you run load tests directly inside PHPUnit — same test runner, same CI pipeline, with assertions for success rate, P95 latency, and error rate to catch performance regressions automatically. ![Load Testing Laravel Applications with PHPUnit and VoltTest](/img/laravel-phpunit-og.png) ## Why Does PHPUnit Integration Matter? Most Laravel developers already rely on PHPUnit for testing logic and features. Now, you can also measure your app’s behavior under load without leaving that familiar workflow. **This means:** - No switching to external tools or YAML scripts. - Run load tests in your CI/CD pipeline. - Catch performance regressions automatically. - Keep your load testing code version-controlled alongside your application tests. ## Installation If you already use the package, update it: ```bash composer update ``` or install fresh ```bash composer require volt-test/laravel-performance-testing ``` you can check the package configuration options and how you can use in the blog post [Effortless Laravel Performance Testing with VoltTest PHP SDK](/blog/effortless-laravel-performance-testing-with-volt-test-php-sdk). ## Configure PHPUnit In your `phpunit.xml` file, add a new testsuite for performance tests: ```xml .... tests/Performance ``` This tells PHPUnit to look for performance tests in the `tests/Performance` directory. Additionally, you can set environment variables for VoltTest in the `phpunit.xml` file: ```xml ``` Environment variables Explanation: - `VOLTTEST_BASE_PATH`: The base path for your Laravel application. (usually `.`) - `VOLTTEST_SERVER_PORT`: The Preferred port where your Laravel app will run during tests. - `VOLTTEST_ENABLE_SERVER_MANAGEMENT`: Enables automatic server management during tests without run you server (usually using this during you writing the test ). - `VOLTTEST_DEBUG_FOR_SERVER_MANAGEMENT`: Enables debug logs for server management (useful for troubleshooting). Alternatively, you can set these variables in your system environment or `.env` or `.env.testing` file. ```bash VOLTTEST_BASE_PATH=. VOLTTEST_SERVER_PORT=8009 VOLTTEST_ENABLE_SERVER_MANAGEMENT=true VOLTTEST_DEBUG_FOR_SERVER_MANAGEMENT=true ``` ## Writing Load Tests with PHPUnit and VoltTest Once installed and configure, you can create a new test file inside your Laravel app’s tests/Performance directory: ```php loadTestUrl('/', [ 'virtual_users' => 50, ]); // Assert that the success rate is above 99% $this->assertVTSuccessful($result, 99); // Assert that the 95th percentile response time is below 150ms $this->assertVTP95ResponseTime($result, 150); } } ``` In this example: - We create a `HomePageLoadTest` class that extends `PerformanceTestCase`. - We enable server management by setting the static property `$enableServerManagement` to `true`. So VoltTest will automatically start and stop your Laravel server during the test. - We define a test method `testHomePageUnderLoad` that runs a load test on the home page (`/`) with 50 virtual users. - We use assertions to verify that the success rate is above 99% and that the 95th percentile response time is below or equal 150 milliseconds. ## Running Your Load Tests To execute your load tests, run PHPUnit with the performance testsuite: ```bash vendor/bin/phpunit --testsuite Performance ``` This command will run all tests in the `tests/Performance` directory, including your load tests. You can also run a specific test file: ```bash vendor/bin/phpunit tests/Performance/HomePageLoadTest.php ``` ### Example Output PHPUnit interleaves VoltTest performance reports with your regular assertions—for example: ```text $ vendor/bin/phpunit --testsuite Performance PHPUnit 11.5.44 by Sebastian Bergmann and contributors. Runtime: PHP 8.4.15 Configuration: /path/to/app/phpunit.xml Performance Report: testHomePageUnderLoad Total Requests: 50 Success Rate: 100.00% Requests/Sec (RPS): 346.19 Avg Latency: 74.2426ms ---------------------------------------------------------------------- 1 / 1 (100%) Time: 00:01.524, Memory: 30.00 MB ``` if you have set up your CI/CD pipeline to run PHPUnit tests, your load tests will automatically be included in the test suite. if you need to run the test to you staging or production environment, you can set the `VOLTTEST_BASE_URL` environment variable before running PHPUnit: As normal phpunit if assertion of the load test failed the test will be marked as failed. so it can help you to catch performance regressions early in your development process. ## Advanced Load Testing Scenarios (Reusing Existing VoltTest Classes) If you've already created load tests using VoltTest's native syntax (covered in [Effortless Laravel Performance Testing with VoltTest PHP SDK](/blog/effortless-laravel-performance-testing-with-volt-test-php-sdk)), you can now reuse those classes inside your PHPUnit tests. Here is an example: ```php title="app/VoltTests/RegisterAndCheckoutTest.php" scenario('EcommerceAPITest') ->dataSource('registration_users.csv', 'sequential'); // Step 1 : Api.register $scenario->step('Api.register') ->post('/api/v1/register', [ 'name' => '${name}', 'email' => '${email}', 'password' => '${password}', 'password_confirmation' => '${password}', ], ['Content-Type' => 'application/json', 'Accept' => 'application/json']) ->extractJson('token', 'token.access_token') // Extract the token for subsequent requests ->expectStatus(201); // Step 2 : Api.products.index $scenario->step('Api.products.index') ->get('/api/v1/products', ['Authorization' => 'Bearer ${token}', 'Content-Type' => 'application/json', 'Accept' => 'application/json']) ->extractJson('id', 'data[0].id') // Extract the first product ID ->expectStatus(200); // Step 4 : Api.cart.add $scenario->step('Api.cart.add') ->post('/api/v1/cart/add', [ 'product_id' => '${id}', // Use the extracted product ID 'quantity' => 1, // Set a default quantity ], ['Authorization' => 'Bearer ${token}', 'Content-Type' => 'application/json', 'Accept' => 'application/json']) ->expectStatus(200); // Step 6 : Api.orders.checkout $scenario->step('Api.orders.checkout') ->post('/api/v1/orders/checkout', [ // No Body needed for checkout ], ['Authorization' => 'Bearer ${token}', 'Content-Type' => 'application/json', 'Accept' => 'application/json']) ->expectStatus(201); } } ``` This VoltTest class simulates user registration, product browsing, adding to cart, and checkout using a CSV data source for user details. You can then create a PHPUnit test that runs this VoltTest class under load to measure its performance and can also verify data integrity: ```php title="tests/Performance/RegisterAndCheckoutTest.php" create([ 'name' => 'Test Product', 'quantity' => 100, ]); $result = $this->runVoltTest(new EcomerceAPITest, [ 'virtual_users' => 10, ]); $this->assertVTSuccessful($result, 100); $this->assertVTErrorRate($result, 0); $this->assertVTMinResponseTime($result, 100); $this->assertVTAverageResponseTime($result, 200); $this->assertVTP95ResponseTime($result, 200); $this->assertVTP99ResponseTime($result, 200); // check the remain product in the inventory after test $this->assertEquals(90, Product::query()->first()->quantity, "Product quantity should be 90 after 10 registrations."); } } ``` When you run this PHPUnit test, it will execute the `RegisterAndCheckout` VoltTest class with 10 virtual users and report the performance metrics alongside your other PHPUnit tests. ```bash ./vendor/bin/phpunit --testsuite=Performance ``` ### Example Output The output will look like this: ```text ./vendor/bin/phpunit --testsuite=Performance PHPUnit 11.5.44 by Sebastian Bergmann and contributors. Runtime: PHP 8.4.15 Configuration: path/to/app/phpunit.xml ---------------------------------------------------------------------- Performance Report: testPerformanceFromExistingClass ---------------------------------------------------------------------- Total Requests: 40 Success Rate: 100.00% Requests/Sec (RPS): 162.91 Avg Latency: 52.854091ms P95 Latency: 72.748667ms P99 Latency: 78.206542ms ---------------------------------------------------------------------- . 1 / 1 (100%) Time: 00:00.787, Memory: 28.00 MB OK (1 test, 13 assertions) ``` In this PHPUnit test: - We create a `RegistrationPerformanceTest` class that extends `PerformanceTestCase`. - We define a test method `testPerformanceFromExistingClass` that runs the `RegistrationTest` VoltTest class with 10 virtual users. - We use various assertions to validate the performance metrics of the test. This approach allows you to leverage existing VoltTest classes while integrating them seamlessly into your PHPUnit test suite. This tests can catch **race conditions** and data integrity issues under load, ensuring your application behaves correctly even during high traffic scenarios. ## What Are the Benefits of PHPUnit Integration? By integrating VoltTest with PHPUnit, you gain several advantages: - **Unified Testing Workflow**: Manage all your tests—unit, feature, and performance—in one place. - **Automated Performance Regression Detection**: Catch performance issues early in your development cycle - **Reusable Test Logic**: Leverage existing VoltTest classes in your PHPUnit tests for consistency and code reuse. - **CI/CD Integration**: Seamlessly include load tests in your continuous integration pipelines. - **Comprehensive Reporting**: Get detailed performance metrics alongside your regular test results. ## What's Next for VoltTest? The PHPUnit integration is just one part of VoltTest's vision for simplifying performance testing. In the near future, we plan to introduce **VoltTest Cloud**—a hosted service that enables you to run large-scale load tests without managing infrastructure. Try VoltTest Cloud Free: Run distributed load tests at scale without managing infrastructure — 500 VUs free, no credit card. ## Conclusion Running load tests directly within PHPUnit using VoltTest simplifies performance testing for Laravel applications. It allows developers to maintain a consistent testing workflow, catch performance regressions early, and ensure their applications can handle real-world traffic. By leveraging existing VoltTest classes, you can also ensure data integrity under load, making your tests more robust and reliable. For the full picture of what the [Laravel load testing package](/solutions/laravel-load-testing) does — route scaffolding, session handling and CI gates — see the overview. ## Learn More - [GitHub Release Notes →](https://github.com/volt-test/laravel-performance-testing/releases/tag/1.2.0) - [Full PHPUnit Integration Docs →](https://github.com/volt-test/laravel-performance-testing/blob/main/docs/PHPUNIT_INTEGRATION.md) - [Previous Article: Effortless Laravel Load Testing with VoltTest PHP SDK →](/blog/effortless-laravel-performance-testing-with-volt-test-php-sdk) - [Stress Testing Laravel with the VoltTest Web UI →](/blog/stress-testing-laravel-with-volt-test-web-ui) --- ⭐ **Star the repository on GitHub:** [volt-test/laravel-performance-testing](https://github.com/volt-test/laravel-performance-testing) 💬 **Follow updates on X:** [@VoltTest](https://x.com/VT_Developers) --- --- ### Effortless Laravel Performance Testing with VoltTest PHP SDK URL: https://volt-test.com/blog/effortless-laravel-performance-testing-with-volt-test-php-sdk Published: 2025-08-09 Keywords: laravel performance testing, laravel load testing package, volt-test laravel sdk, php performance testing tool # Effortless Laravel Performance Testing — Without Leaving PHP When your Laravel app hits real traffic, will it *fly*… or will it *fall over*? Most load-testing tools make you jump through hoops — learn a new scripting language, spin up external services, or fight with configs that feel like they belong to another ecosystem. That’s why I built the **Laravel Performance Testing package**: a native, PHP-first way to run **load** and **stress tests** right inside your Laravel project — powered by the [VoltTest PHP SDK](https://docs.volt-test.com). You write your tests in plain PHP, keep them version-controlled with your codebase, and run them with a single [Artisan](https://laravel.com/docs/artisan) command. No context-switching. No external scripts. Just Laravel, PHP, and the performance insights you need before your users find the bottlenecks. TL;DR: The VoltTest Laravel package lets you write and run load tests inside your Laravel project with Artisan commands, route-based test generation, and CSV data sources — no external tools or languages needed. ![Effortless Laravel Performance Testing with VoltTest PHP SDK](/img/effortless-laravel-og.png) ## Why Was This Package Created? About a month ago, I released this package to make performance testing in Laravel: - **Easier** – no external scripts or frameworks to learn. - **Native** – tests live inside your Laravel project. - **Flexible** – from simple single-URL load tests to complex multi-step scenarios. --- ## What Features Does the Package Include? - **Laravel-friendly integration** – Works seamlessly with routes, middleware, and config. - **Artisan commands** – Generate and run tests directly from the CLI. - **Automatic route discovery** – Quickly build test scenarios from your existing routes. - **Variable extraction** – Reuse cookies, headers, JSON fields, and HTML values between steps. - **Data-driven testing** – Feed test data from CSV files for realistic simulations. - **Detailed metrics** – Success rate, RPS, average latency, P95 latency, and more. - **Report storage** – Save test results for later analysis. --- ## Installation Install via [Composer](https://getcomposer.org/): ```bash composer require volt-test/laravel-performance-testing ``` Publish the configuration file: ```bash php artisan vendor:publish --tag=volttest-config ``` This creates `config/volttest.php` where you can tweak settings like default virtual users, duration, and report paths. --- ## Quick Start You can run a quick performance test without writing any code: ```bash php artisan volttest:run https://example.com/api/login --users=100 --method=POST --body='{"email":"test@example.com","password":"secret"}' ``` Or, you can create a reusable test class: ```bash php artisan volttest:make ExampleTest ``` This generates `app/VoltTests/ExampleTest.php`: ```php namespace App\VoltTests; use VoltTest\Laravel\Contracts\VoltTestCase; use VoltTest\Laravel\VoltTestManager; class ExampleTest implements VoltTestCase { public function define(VoltTestManager $manager): void { $manager->scenario('ExampleTest') ->step('Visit Home Page') ->get('https://example.com') ->validateStatus('success', 200); } } ``` Run it with: ```bash php artisan volttest:run ExampleTest --users=50 --duration=30 ``` --- ## Route-Based Test Generation Skip manual coding by letting the package generate tests from your Laravel routes. ```bash # Include all routes php artisan volttest:make ApiTest --routes # Filter by pattern php artisan volttest:make ApiTest --routes --filter="api/*" # Only GET routes php artisan volttest:make ApiTest --routes --method=GET # Only authenticated routes php artisan volttest:make ApiTest --routes --auth # Interactive selection php artisan volttest:make ApiTest --routes --select ``` --- ## Data-Driven Testing Simulate realistic usage with CSV files: **users.csv** ```csv name,email,password John Doe,user1@example.com,password123 Jane Smith,user2@example.com,password456 ``` **Test definition:** ```php $manager->scenario('RegisterTest') ->dataSource('users.csv') ->step('Register User') ->post('/register', [ 'name' => '${name}', 'email' => '${email}', 'password' => '${password}', ]); ``` --- ## Extract and Reuse Values You can capture values from responses and reuse them in later steps. **CSRF token from HTML** ```php $scenario->step('Get Login Page') ->get('/login') ->extractCsrfToken('csrf_token'); $scenario->step('Submit Login') ->post('/login', [ '_token' => '${csrf_token}', 'email' => 'user@example.com', 'password' => 'secret', ]); ``` **JSON field** ```php $scenario->step('Get User') ->get('/api/user') ->extractJson('user_id', 'data.id'); ``` **Header** ```php $scenario->step('Get Token') ->get('/auth') ->extractHeader('Authorization', 'Bearer ${token}'); ``` --- ## Analyzing Results After running a test, you’ll see metrics such as: - Success rate - Requests per second (RPS) - Average & P95 latency - Duration - Errors If `save_reports` is enabled, you’ll find detailed reports in: ``` storage/volttest/reports ``` --- ## Conclusion With **Laravel Performance Testing** powered by the **VoltTest PHP SDK**, you can: - Keep performance tests right inside your Laravel project. - Run them with one simple Artisan command. - Get detailed, actionable performance metrics before your users ever notice a slowdown. No separate scripting language. No complex setup. Just **Laravel, PHP, and the truth about your app’s performance**. For a comprehensive comparison of PHP load testing tools and when to use each, see our [PHP Load Testing guide](/blog/php-load-testing). A full walkthrough of the [Laravel package and its Artisan commands](/solutions/laravel-load-testing) is also available. If you need to push past expected traffic to find breaking points, check out the [PHP Stress Testing Tool guide](/blog/php-stress-testing-tool). **Docs:** [Laravel Performance Testing on GitHub](https://github.com/volt-test/laravel-performance-testing) **VoltTest PHP SDK:** [docs.volt-test.com](https://docs.volt-test.com) Pro Tip: Integrate VoltTest into your CI pipeline to catch performance regressions before they hit production. See [Load Testing Laravel with PHPUnit](/blog/laravel-load-testing-with-phpunit) for a step-by-step guide. Try VoltTest Cloud Free: Run distributed load tests at scale without managing infrastructure — 500 VUs free, no credit card. --- ### Performance Testing: Types and Differences with Examples URL: https://volt-test.com/blog/performance-testing-types Published: 2025-03-03 Keywords: performance testing types, load testing vs stress testing, spike testing, soak testing # Performance Testing: Types and Differences with Examples Performance testing is an essential process in software development that evaluates a system's responsiveness, stability, and scalability under different conditions. It helps identify bottlenecks and ensures applications meet performance expectations before deployment. There are several types of performance testing, each serving a different purpose. By understanding these types, you can choose the right tests to conduct based on your application's requirements. In this article, we will explore these types, their differences, and provide practical examples. TL;DR: Performance testing comes in seven main types — load, stress, spike, endurance, scalability, volume, and latency — each answering a different question about your application's behavior under pressure. ## What Is Load Testing? ### Definition Load testing measures a system's behavior under `expected` user loads. It ensures that the system can handle a specified number of users or transactions simultaneously without degradation in performance. For a hands-on guide to running load tests on PHP applications, see [PHP Load Testing: How to Load Test PHP Applications](/blog/php-load-testing). ### Example A SaaS-based school management system is expected to support 10,000 concurrent students during exam submission. A load test simulates these 10,000 users submitting their answers within a short period to evaluate how the system handles the load. So Here You know the expected load, you are testing the system under that load. ## What Is Stress Testing? ### Definition Stress testing evaluates a system's behavior under extreme conditions, often `beyond its normal operational capacity`. It helps determine the breaking point and whether the system fails gracefully. For a practical stress testing walkthrough, see [PHP Stress Testing Tool: How to Stress Test PHP & Laravel Apps](/blog/php-stress-testing-tool). ### Example A cloud-based e-commerce platform is expected to handle 100,000 users during peak shopping hours. A stress test increases the load gradually to 200,000 users to observe if the system crashes or recovers after failure. ## What Is Spike Testing? ### Definition Spike testing analyzes how a system handles sudden and extreme increases in load. It helps identify whether the system scales effectively or crashes under sudden traffic spikes. ### Example A ticket booking system for a concert sees an abrupt spike in traffic when tickets go on sale. A spike test simulates a rapid increase from 1,000 to 50,000 users within a minute to test system stability. ## What Is Endurance Testing? ### Definition Endurance testing evaluates a system's performance over an extended period under normal expected loads. It detects memory leaks, slowdowns, and other long-term performance issues. ### Example A banking application undergoes endurance testing by simulating `5,000 users performing transactions continuously over 24 hours` to ensure `there are no memory leaks` or `performance degradation`. ## What Is Scalability Testing? ### Definition Scalability testing assesses a system's ability to handle increased loads by adding resources. It helps determine how the system scales vertically or horizontally to accommodate more users or transactions. ### Example A cloud-based video streaming platform scales horizontally by adding more servers to handle increased user traffic during peak hours. Or it scales vertically by upgrading server resources to improve performance. then downgrades the resources when the traffic decreases. ## What Is Volume Testing? ### Definition Volume testing measures how the system performs with a large volume of data. It helps identify database performance issues, indexing problems, and storage limitations. ### Example A CRM system undergoes volume testing by loading `1 million customer records` to evaluate how the system handles the data volume. It checks if the system slows down, crashes, or maintains performance with the large dataset. ## What Is Latency Testing? ### Definition Latency testing evaluates a system's response time under different network conditions. It helps identify delays in data transmission, network congestion, and server processing times. ### Example A real-time multiplayer game undergoes latency testing to measure the time it takes for player actions to reflect on other players' screens. It simulates high latency, packet loss, and network congestion to observe how the game performs under these conditions. ## What Are the Differences Between Performance Testing Types? | Type | Tests For | Load Level | Duration | Key Question | |------|-----------|-----------|----------|--------------| | Load | Expected traffic | Normal | Minutes–hours | Can we handle Tuesday at 2 pm? | | Stress | Breaking point | Beyond limits | Minutes | Where does it break? | | Spike | Sudden surges | Sudden extreme | Seconds–minutes | Does it survive a flash crowd? | | Endurance | Long-term stability | Normal, sustained | Hours–days | Do memory leaks appear over time? | | Scalability | Resource scaling | Incrementally increasing | Variable | Does adding servers help? | | Volume | Data volume | Normal | Variable | Does performance degrade with 1 M rows? | | Latency | Response delays | Variable | Variable | How fast does the user see a response? | - **Load Testing vs. Stress Testing**: Load testing evaluates a system under expected loads, while stress testing pushes the system beyond its limits. - **Spike Testing vs. Endurance Testing**: Spike testing simulates sudden load spikes, while endurance testing evaluates long-term performance. - **Scalability Testing vs. Volume Testing**: Scalability testing assesses the system's ability to scale, while volume testing measures performance with large data volumes. - **Latency Testing**: Latency testing evaluates response times under different network conditions. These categories align with the [ISTQB performance testing taxonomy](https://www.istqb.org/), the industry-standard classification for software testing disciplines. ## Conclusion Performance testing is crucial for ensuring software applications meet performance expectations and deliver a seamless user experience. By understanding the different types of performance testing and their purposes, you can identify the right tests to conduct based on your application's requirements. Whether you need to evaluate expected loads, extreme conditions, scalability, or latency, performance testing helps you optimize your system for peak performance. Ready to put this into practice? Start with our [PHP load testing guide](/blog/php-load-testing) or learn how to [stress test Laravel applications step by step](/blog/stress-testing-laravel-with-volt-test-web-ui). For background on how VoltTest approaches performance testing with a PHP-native interface, see [Introducing VoltTest](/blog/introducing-volt-test-php-load-testing), or browse the [use-case guides for PHP APIs, Laravel apps and e-commerce flows](/solutions). --- ### Stress Testing Laravel Applications with VoltTest (Web UI Flow) URL: https://volt-test.com/blog/stress-testing-laravel-with-volt-test-web-ui Published: 2025-02-28 Keywords: stress testing laravel, laravel load testing, laravel performance testing, volt-test laravel # Introduction In this tutorial, we will explore how to perform stress testing on a Laravel application using the VoltTest PHP SDK. You will learn how to: * Simulate multiple users interacting with your Laravel application. * Test user registration and authentication workflows. * Measure application performance under load. * Extract dynamic values like CSRF tokens during test execution. * Use a CSV file as a data source for testing. * Analyze and optimize performance bottlenecks. By the end of this guide, you will be able to confidently run automated performance tests to ensure your Laravel app is scalable and resilient. TL;DR: This tutorial shows how to stress test a Laravel web application with VoltTest — simulating user registration, CSRF token extraction, and dashboard access under concurrent load using CSV-driven test data. ![Stress test Laravel web UI flows with VoltTest: multi-step user journeys with CSRF and sessions handled](/img/stress-testing-laravel-og.png) ## Prerequisites Ensure you have the following: - A running Laravel application - PHP 8.0 or higher - Composer installed ## Installing VoltTest PHP SDK First, install the VoltTest PHP SDK via Composer in your Laravel project: ```bash title="Terminal" composer require volt-test/php-sdk ``` Or clone the repository: ```bash title="Terminal" git clone https://github.com/volt-test/php-sdk.git cd php-sdk composer install ``` **For _Windows_ users,** Visit the installation guide [here](https://docs.volt-test.com/docs/installation#running-on-windows) ## Creating Test Data For a realistic test, create a CSV file (`users.csv`) with test user data: ```csv title="users.csv" email,password user1@example.com,password123 user2@example.com,password123 user3@example.com,password123 user4@example.com,password123 user5@example.com,password123 ``` Place this file in your project directory. ## Writing the Stress Test Create a file named `laravel_stress_test.php` in your project root and add the following script (or you can use the laravel's command): ```php title="laravel_stress_test.php" setVirtualUsers(5) ->setDuration('60s') // Optional: Set test duration ->setRampUp('10s') // Optional: Set ramp-up time ->setHttpDebug(true); // Enable HTTP debug logging for checking requests before sending the all requests // Create test scenario $userFlowScenario = $test->scenario('User Registration Flow') ->autoHandleCookies(); // Automatically handle cookies without extract them // Set up data source $userFlowScenario->setDataSourceConfiguration( new DataSourceConfiguration(__Dir__ . '/users.csv', 'unique', true) // Load data from CSV file, should be the full path ); // Step 1: Visit home page $userFlowScenario->step('Visit Home Page') ->get('http://localhost:8000') ->header('Accept', 'text/html') ->validateStatus('home_page_loaded', 200) ->setThinkTime('2s'); // Step 2: Visit register page and extract CSRF token $userFlowScenario->step('Visit Register Page') ->get('http://localhost:8000/register') ->header('Accept', 'text/html') ->extractFromHtml('csrf_token', 'input[name="_token"]', 'value') ->validateStatus('register_page_loaded', 200) ->setThinkTime('2s'); // Step 3: Submit registration form $userFlowScenario->step('Submit Registration') ->post( 'http://localhost:8000/register', '_token=${csrf_token}&name=Test User&email=${email}&password=${password}&password_confirmation=${password}' ) ->header('Content-Type', 'application/x-www-form-urlencoded') ->validateStatus('registration_successful', 302) ->setThinkTime('1s'); // Step 4: Access dashboard $userFlowScenario->step('Visit Dashboard') ->get('http://localhost:8000/dashboard') ->header('Accept', 'text/html') ->validateStatus('dashboard_loaded', 200) ->setThinkTime('3s'); // Run test $result = $test->run(true); // true enables real-time progress output // Display results summary in case you turned off the real-time progress output echo "\n\nTest Results Summary:\n"; echo "====================\n"; echo "Total Requests: " . $result->getTotalRequests() . "\n"; echo "Success Rate: " . $result->getSuccessRate() . "%\n"; echo "Requests/second: " . $result->getRequestsPerSecond() . "\n"; echo "Min Response Time: " . $result->getMinResponseTime() . "\n"; echo "Max Response Time: " . $result->getMaxResponseTime() . "\n"; echo "Avg Response Time: " . $result->getAvgResponseTime() . "\n"; echo "P95 Response Time: " . $result->getP95ResponseTime() . "\n"; ``` ### Code Explanation The stress test script simulates multiple users interacting with a Laravel web application. Here’s how it works: 1. **Initialize VoltTest**: ```php title="laravel_stress_test.php" $test = new VoltTest( 'Laravel User Flow', 'Tests home page visit, user registration, and dashboard access' ); ``` - Creates a new test instance with a descriptive name. 2. **Configure test parameters**: ```php title="laravel_stress_test.php" $test ->setVirtualUsers(5) ->setDuration('60s') ->setRampUp('10s') ->setHttpDebug(true); ``` - **5 virtual users**: Simulate 5 concurrent users. - **Running the test for 60 seconds**: Optional - you can remove this line to run the test and finish when all virtual users finish their scenarios. - **Ramping up over 10 seconds**: Optional - you can remove this line to start all virtual users at the same time. - **Enables HTTP debug logging**: Optional - Inspect requests while sending them. 3. **Create a test scenario**: ```php title="laravel_stress_test.php" $userFlowScenario = $test->scenario('User Registration Flow') ->autoHandleCookies(); ``` - Creates a scenario for user registration flow. - **autoHandleCookies()**: Automatically handles cookies without extracting them. 4. **Set up data source**: ```php title="laravel_stress_test.php" $userFlowScenario->setDataSourceConfiguration( new DataSourceConfiguration(__Dir__ . '/users.csv', 'unique', true) ); ``` - Loads user data from `users.csv` file. To simulate different users for each virtual user. 5. **Define test steps**: ```php title="laravel_stress_test.php" $userFlowScenario->step('Visit Home Page') ->get('http://localhost:8000') ->header('Accept', 'text/html') ->validateStatus('home_page_loaded', 200) ->setThinkTime('2s'); ``` - **Visit Home Page**: Sends a GET request to the home page. - **Accept header**: Optional - Specifies the expected response content type. - **Validate Status**: Optional - Checks if the response status is 200. - **Think Time**: Optional - Simulates user thinking time before the next step, so the virtual user will wait 2s before execute the next request. 6. **Extract CSRF token from registration page**: ```php title="laravel_stress_test.php" $userFlowScenario->step('Visit Register Page') ->get('http://localhost:8000/register') ->header('Accept', 'text/html') ->extractFromHtml('csrf_token', 'input[name="_token"]', 'value') ->validateStatus('register_page_loaded', 200) ->setThinkTime('2s'); ``` - Visits the registration page. - Extracts the [CSRF token](https://laravel.com/docs/csrf) from the HTML response. The token is stored in the variable `${csrf_token}`. - The way to extract the token is by using the `extractFromHtml` method, which takes the token name, the selector, and the attribute name. 7. **Submit registration form**: ```php title="laravel_stress_test.php" $userFlowScenario->step('Submit Registration') ->post( 'http://localhost:8000/register', '_token=${csrf_token}&name=Test User&email=${email}&password=${password}&password_confirmation=${password}' ) ->header('Content-Type', 'application/x-www-form-urlencoded') ->validateStatus('registration_successful', 302) ->setThinkTime('1s'); ``` - Submits the registration form with the extracted CSRF token and user data. - Validates the response status code. - Sets a 1-second think time before the next step. - The `${email}` and `${password}` variables are replaced with values from the CSV file. - The `${csrf_token}` variable is replaced with the extracted CSRF token. - The `validateStatus` method checks if the response status is 302 (redirect). - The `setThinkTime` method simulates user thinking time before the next step. 8. **Access dashboard**: ```php title="laravel_stress_test.php" $userFlowScenario->step('Visit Dashboard') ->get('http://localhost:8000/dashboard') ->header('Accept', 'text/html') ->validateStatus('dashboard_loaded', 200) ->setThinkTime('3s'); ``` Ensures successful login by verifying dashboard access. 9. **Run the test**: ```php title="laravel_stress_test.php" $result = $test->run(true); ``` - Executes the test with real-time progress output. ## Running the Test Execute the script: ```bash php laravel_stress_test.php ``` You'll see real-time progress, followed by a summary of test results. ## Analyzing Results ### Key Metrics: - **Success Rate**: Should be as close to 100% as possible - **Response Times**: Lower is better - **Requests per Second**: Higher means better scalability ## What Are Common Stress Testing Issues and How Do You Fix Them? ### Low Success Rate - **Possible Cause**: Server overload - **Solution**: Reduce virtual users or optimize queries ### High Response Times - **Possible Cause**: Database bottlenecks - **Solution**: Optimize queries, enable caching ### CSRF Token Extraction Fails - **Possible Cause**: HTML structure changes - **Solution**: Update the selector ## Next Steps - Expand tests to cover more user flows - Test with more virtual users — see our [PHP stress testing tool guide](/blog/php-stress-testing-tool) for scaling strategies - [Automate load tests in your PHPUnit CI pipeline](/blog/laravel-load-testing-with-phpunit) - Scaffold tests straight from your route table with the [Laravel package](/solutions/laravel-load-testing) ## Conclusion Using VoltTest for stress testing helps identify Laravel application bottlenecks before they impact users. Regular testing ensures scalability and reliability. For a deeper dive into the different types of performance testing (load, stress, spike, endurance), see our [performance testing types guide](/blog/performance-testing-types). Or jump straight to our comprehensive [PHP load testing guide](/blog/php-load-testing) for the full picture. Happy testing! ## Get in Touch For questions, discussions, or contributions, feel free to open an issue or start a discussion on the VoltTest GitHub repository: [https://github.com/volt-test/php-sdk](https://github.com/volt-test/php-sdk) Try VoltTest Cloud Free: Run distributed load tests at scale without managing infrastructure — 500 VUs free, no credit card. --- ### Introducing VoltTest: Stress & Load Testing for PHP Developers URL: https://volt-test.com/blog/introducing-volt-test-php-load-testing Published: 2025-02-26 Keywords: volt-test php sdk, php load testing, php performance testing, php stress testing tool # Introducing VoltTest PHP SDK: Seamless Performance Testing for PHP Applications Performance testing is crucial in modern application development, yet many developers skip it due to complex tools that don’t fit into PHP workflows. Today, I’m excited to introduce **VoltTest PHP SDK**, a powerful load testing tool with a PHP-native interface backed by Go’s exceptional performance capabilities. TL;DR: VoltTest is a PHP-native load testing SDK powered by a Go engine. Write tests in PHP, install via Composer, and simulate concurrent users without learning a new language or spinning up external tools. ## What Problem Does VoltTest Solve? As a PHP developer, I’ve often faced a common challenge: existing performance testing tools often require learning a new language, platform, or configuration syntax. This cognitive overhead leads many teams to defer proper load testing or rely on third-party services, creating additional costs and dependencies. VoltTest bridges this gap by providing: - A **fluent, intuitive PHP API** that feels natural to PHP developers - The **raw power of Go** for generating heavy loads with minimal resources - **No additional infrastructure** requirements beyond your existing PHP environment - **Comprehensive metrics** to identify bottlenecks and performance issues ## How Does VoltTest Work? VoltTest employs a unique dual-language architecture that delivers the best of both worlds: 1. Your **PHP code** defines test scenarios, configurations, and validation rules 2. The **SDK** transforms these definitions into a format the Go engine understands 3. The **Go engine** executes the actual load testing with true concurrency 4. **Results** are streamed back to your PHP application for analysis This architecture allows you to write and maintain tests in familiar PHP syntax while benefiting from Go's exceptional performance characteristics for the load generation. ## What Are VoltTest's Key Features? - **Fluent API**: Natural, chainable PHP methods for defining test scenarios - **Multi-scenario support**: Test different user flows with configurable weight distribution - **Variable extraction**: Capture and reuse values from responses (cookies, headers, JSON paths and HTML) - **Data-driven testing**: Feed tests with CSV files to simulate realistic user data - **Detailed metrics**: Get comprehensive performance insights including response times, throughput, and error rates ## Getting Started with VoltTest Let's create a simple performance test to demonstrate VoltTest's capabilities: ```php setVirtualUsers(50) // Number of concurrent users ->setDuration('30s') // Test duration ->setRampUp('5s') // Gradually ramp up to full load ->setHttpDebug(false); // Disable HTTP debug output // Create a test scenario $apiScenario = $test->scenario('API Flow') ->setWeight(100); // Full weight (100%) to this scenario // Define the first step - Get auth token $apiScenario->step('Get Token') ->post('https://api.example.com/auth', '{"username":"test","password":"test123"}') ->header('Content-Type', 'application/json') ->extractFromJson('token', 'data.token') // Extract token from response ->validateStatus('success', 200); // Define the second step - Use the token $apiScenario->step('Get User Data') ->get('https://api.example.com/users/me') ->header('Authorization', 'Bearer ${token}') // Use extracted token ->validateStatus('success', 200); // Run the test $result = $test->run(true); // true enables real-time progress output // Display results echo "Success Rate: " . $result->getSuccessRate() . "%\n"; echo "Requests/sec: " . $result->getRequestsPerSecond() . "\n"; echo "Avg Response: " . $result->getAvgResponseTime() . "\n"; echo "P95 Response: " . $result->getP95ResponseTime() . "\n"; ``` ## Advanced Features: Testing with Real-World Data VoltTest supports data-driven testing with CSV files, allowing you to simulate diverse user behaviors: ```php // users.csv contains columns: email,password,user_id $loginScenario->setDataSourceConfiguration( new DataSourceConfiguration('users.csv', 'random', true) ); // Now you can reference CSV columns in your requests $loginScenario->step('Login') ->post('https://example.com/login', 'email=${email}&password=${password}') // Variables from CSV ->validateStatus('success', 200); ``` ## Extracting and Using Dynamic Values One of VoltTest's most powerful features is its ability to extract and reuse values from responses: ```php // Extract CSRF token from HTML response $scenario->step('Get Login Page') ->get('https://example.com/login') ->extractFromHtml('csrf_token', 'input[name="_token"]', 'value') ->validateStatus('success', 200); // Use the extracted token in the next request $scenario->step('Submit Login') ->post('https://example.com/login', '_token=${csrf_token}&email=user@example.com&password=secret') ->validateStatus('success', 302); // Expecting a redirect ``` ## Understanding Your Results VoltTest provides comprehensive metrics to analyze performance: ```php $result = $test->run(); // Access detailed metrics echo "Test duration: " . $result->getDuration() . "\n"; echo "Total requests: " . $result->getTotalRequests() . "\n"; echo "Success rate: " . $result->getSuccessRate() . "%\n"; echo "Requests/second: " . $result->getRequestsPerSecond() . "\n"; echo "Min response time: " . $result->getMinResponseTime() . "\n"; echo "Max response time: " . $result->getMaxResponseTime() . "\n"; echo "Avg response time: " . $result->getAvgResponseTime() . "\n"; echo "Median response time: " . $result->getMedianResponseTime() . "\n"; echo "P95 response time: " . $result->getP95ResponseTime() . "\n"; echo "P99 response time: " . $result->getP99ResponseTime() . "\n"; ``` ## Why Was VoltTest Created? As PHP developers, I’ve often found performance testing to be a challenge. Existing tools like [JMeter](https://jmeter.apache.org/), [Gatling](https://gatling.io/) or [Locust](https://locust.io/) are powerful but require learning new syntax, languages or complex GUIs. We wanted a tool that would: 1. **Feel native to PHP developers** with an intuitive, fluent API 2. **Handle serious load testing** without requiring huge resources 3. **Integrate seamlessly** with PHP codebases and workflows 4. **Provide comprehensive metrics** with minimal configuration VoltTest achieves this through a unique architecture that lets you write your tests in familiar PHP syntax while leveraging Go's exceptional performance capabilities for the actual load generation. ## What Makes VoltTest Different? Unlike traditional PHP-based testing tools that struggle with concurrency limitations, VoltTest uses a high-performance Go engine that runs behind the scenes. This gives you: - **True concurrent users** instead of PHP's process-based concurrency - **Minimal resource usage** even when simulating thousands of users - **Accurate timing and metrics** collection for detailed analysis VoltTest brings modern stress testing to PHP with an intuitive API and high-performance Go engine. No new languages, no complex setup—just performance insights when you need them. ## Conclusion VoltTest PHP SDK brings enterprise-grade performance testing capabilities to PHP developers with a familiar, easy-to-use API. By combining PHP’s developer-friendly syntax with Go’s raw performance power, VoltTest enables teams to integrate load testing directly into their development workflows without additional infrastructure or expertise. Whether you’re building APIs, websites, or complex applications, VoltTest gives you the tools to ensure your PHP applications can handle real-world traffic with confidence. ## Getting Started 💡 Ready to test your PHP app’s performance? Try VoltTest today! 🚀 ```bash composer require volt-test/php-sdk ``` For more examples and detailed documentation, visit [Examples Page in docs](https://docs.volt-test.com/docs/category/examples). ## Keep Reading - [PHP Load Testing: The Complete Guide →](/blog/php-load-testing) - [Stress Testing Laravel Applications (Web UI Flow) →](/blog/stress-testing-laravel-with-volt-test-web-ui) - [Load Testing Laravel with PHPUnit →](/blog/laravel-load-testing-with-phpunit) - [What the VoltTest platform does →](/features) - [How VoltTest compares to other load testing tools →](/compare)