Skip to content

Laravel load testing tool

A Composer package and two Artisan commands. Scaffold a test from the routes your application already defines, run it against a real server, and get percentile latency back — without leaving PHP or adding a second toolchain to your repository.

It is a Laravel performance testing tool that lives inside the project: tests are ordinary classes in app/VoltTests, reviewed in the same pull request as the code they exercise.

500 VUs free, no credit card

Why Laravel needs its own load testing tool

A Laravel application that passes every unit, feature and browser test can still fall over at two hundred concurrent users, because none of those tests run concurrently. The failures are structural: an eager-load you forgot turns into a query storm, the connection pool empties before CPU looks busy, sessions contend in Redis, and the queue fills faster than workers drain it. You do not find any of that by clicking through the app.

A generic load generator can reproduce the traffic, but you spend the first day teaching it what Laravel already knows — where the routes are, how the session cookie works, where the CSRF token lives. A Laravel load testing tool starts from your route table and your config instead, so the first useful test is minutes away rather than an afternoon.

N+1 under concurrency

Invisible at one request per second, quadratic when the index route is hit two hundred times at once.

Connection pool exhaustion

Requests queue behind the pool limit long before CPU or memory look like the problem.

Session and cache contention

Every signed-in user touches the session store on every request. Alone it is nothing; together it is a bottleneck.

Queue backpressure

Jobs enqueue faster than workers drain them, and latency drifts upward over the length of the run.

For a deeper walkthrough of each failure mode and how to reproduce it, the full Laravel load testing guide covers the database, session, queue and PHP-FPM layers in detail.

What a Laravel test looks like

One class, one method. Paths are app-relative, bodies are PHP arrays, and each virtual user keeps its own session for the whole run — so the dashboard request at the end really is authenticated.

app/VoltTests/CheckoutTest.php
<?php

namespace App\VoltTests;

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

class CheckoutTest implements VoltTestCase
{
    public function define(VoltTestManager $manager): void
    {
        $scenario = $manager->scenario('Checkout');

        // cookies are already handled — take the CSRF token
        $scenario->step('Login page')
            ->get('/login')
            ->expectStatus(200)
            ->extractCsrfToken();

        // array body, form-encoded for you
        $scenario->step('Sign in')
            ->post('/login', [
                '_token'    => '${csrf_token}',
                'email'    => '${email}',
                'password' => '${password}',
            ])
            ->expectStatus(302)
            ->thinkTime('2s');

        // still the same virtual user, still signed in
        $scenario->step('Dashboard')
            ->get('/dashboard')
            ->expectStatus(200);
    }
}
Flow diagram: artisan volttest:make reads the route table and writes a VoltTestCase class, which artisan volttest:run executes locally or with --cloud, producing a console summary and a JSON report; a parallel PHPUnit path runs the same class and asserts on p95 to pass or fail CI.
The same test class runs two ways: Artisan while you iterate, PHPUnit when CI needs a pass or fail.

What is automatic, and what is one line

  • Sessions and cookies are automatic. Every scenario enables cookie handling, so each virtual user holds its own Laravel session without you writing anything.
  • CSRF is one line, not zero. Call extractCsrfToken() on the step that fetched the form. When the scaffolder generates a web POST it writes the ${csrf_token} placeholder but not the step that fills it — you add that once.

How to run it

Three commands from an existing Laravel application to a finished run.

  1. 01

    Install the package

    A dev dependency, nothing more. Laravel 11, 12 or 13 on PHP 8.2+, plus the pcntl extension — macOS, Linux and WSL are fine; native Windows is not.

    terminal
    composer require volt-test/laravel-performance-testing --dev
  2. 02

    Scaffold a test

    Writes app/VoltTests/CheckoutTest.php. Add --routes to generate a step per route from your real route table, and narrow it with --filter, --method, --auth or --select. Good to know: route discovery does not exclude package routes, so Telescope and Horizon show up unless you filter them out.

    terminal
    php artisan volttest:make Checkout --routes --filter=checkout/*
  3. 03

    Run it

    The argument is the class name, not a file path. Add --cloud to run the identical class on managed infrastructure instead of your machine.

    terminal
    php artisan volttest:run CheckoutTest --users=200 --duration=5m

The summary prints to the console when the run finishes, and a JSON report lands in storage/volttest/reports so you can diff runs over time. Cloud runs report to the dashboard instead of writing either.

output
Test Results Summary:
=====================
Duration: 5m0.284s
Total Requests: 61240
Success Rate: 99.91%
Requests per Second: 204.13
Success Requests: 61185
Failed Requests: 55

Response Time:
-------------
Min: 38.417ms
Max: 1.902s
Avg: 214.663ms
Median: 168.204ms
P95: 588.331ms
P99: 1.147s

Example output showing the shape of the summary. Your figures depend entirely on the application under test.

What the Laravel package adds

Everything below is Laravel-specific — the work a general-purpose load generator would leave to you.

Two Artisan commands

volttest:make scaffolds a test class; volttest:run executes it. That is the whole surface area — no separate binary to install, no config server to stand up, no second repository for performance tests.

Scaffold from your real routes

Pass --routes and it reads your route table and writes a step per route, turning {id} segments into ${id} variables. Narrow it with --filter, --method, --auth, or pick interactively with --select.

Sessions handled for you

Every scenario enables cookie handling automatically, so each virtual user holds its own Laravel session for its whole run. CSRF is one call — extractCsrfToken() on the step that loaded the form.

Assertions that fail a build

Extend PerformanceTestCase and assert on the result: a p95 ceiling, a maximum error rate, a minimum sustained requests-per-second. A breach fails the PHPUnit test, which fails the pipeline.

Relative URLs and array bodies

Steps take app-relative paths, prefixed with your configured base URL, and accept PHP arrays as request bodies — JSON- or form-encoded depending on the headers. It reads like Laravel, not like a load-testing DSL.

Same file, local or cloud

Run it on your laptop while you iterate, then add --cloud to execute the identical class on managed infrastructure, optionally split across regions. Nothing in the test file changes.

Four places to point it first

If you only ever write four Laravel load tests, make them these.

01

Login and an authenticated journey

Load the form, take the CSRF token, post credentials, then walk the pages behind the auth middleware. This is where session-store contention and cache stampedes show up.

02

The route everyone hits

Your dashboard or index page, concentrated. An eager-load you forgot costs nothing at one request per second and saturates the connection pool at two hundred.

03

A write that dispatches jobs

Checkout, signup, upload — anything that queues work. Watch whether the queue drains as fast as it fills, or whether latency climbs quietly over the length of the run.

04

Mixed traffic with weights

Several scenarios running side by side in production-like proportions. Endpoints that are fast alone often are not once they compete for the same database and Redis.

Fail the build when p95 regresses

Extend PerformanceTestCase, run the same class you already wrote, and assert on the result. A breach fails the PHPUnit test, which fails the pipeline — no bespoke reporting step in between.

assertVTP95ResponseTime

ceiling on the tail users feel

assertVTErrorRate

maximum share of failed requests

assertVTMinimumRPS

floor on sustained throughput

assertVTSuccessful

minimum overall success rate

Use PHPUnit for gating, not the Artisan command. volttest:run prints its summary and exits successfully whatever the numbers say — it is built for iterating, not for failing a pipeline. See the full assertion reference.

Common questions

Do I have to restructure my Laravel app to use it?+
No. It installs as a dev dependency and reads your existing routes through the Route facade — nothing about your application changes. Tests live in app/VoltTests as ordinary PHP classes implementing a single-method interface, so they sit in your repository next to the code they exercise and get reviewed in the same pull request.
Which Laravel and PHP versions are supported?+
Laravel 11, 12 and 13 on PHP 8.2 or newer. The package also needs the pcntl extension, which is a Unix-only extension — so the tests run on macOS, Linux and WSL, but not on native Windows.
Is CSRF really handled automatically?+
Sessions and cookies are: every scenario turns on automatic cookie handling, so each virtual user keeps its own Laravel session for the length of its run without you writing anything. CSRF is one line rather than zero — call extractCsrfToken() on the step that fetched the form, then reference ${csrf_token} in the POST that follows. Worth knowing: when the scaffolder generates a web POST it writes the ${csrf_token} placeholder but not the step that extracts it, so you add that step yourself the first time.
Can I fail a CI build when performance regresses?+
Yes, through PHPUnit. Extend PerformanceTestCase (or pull in the VoltTestAssertions trait), run your test class with runVoltTest(), and assert on the result with assertVTP95ResponseTime, assertVTErrorRate, assertVTMinimumRPS and the rest — a failed assertion fails the test like any other. Note that the artisan command is not the tool for this: php artisan volttest:run prints its summary and exits successfully regardless of the numbers.
Does it hit my real database?+
Yes — it drives real HTTP requests against a running application, so every query, cache read and queued job happens for real. That is the point: the failures worth finding under load are the ones that only appear when two hundred requests contend for the same connection pool. Point it at a staging environment that mirrors production rather than at production itself.
How many virtual users can I run for free?+
The free tier includes 500 virtual users and needs no credit card, which is enough to surface most connection-pool, N+1 and queue-backpressure problems in a Laravel application. Local runs have no VU ceiling beyond your own machine. See the pricing page for what is available beyond the free tier.

Written by Islam A-Elwafa, founder of VoltTest and author of the PHP SDK, the Laravel package and the Go engine · Last updated August 2026

Ready to push your limits?

Start with 500 VUs, real cloud infrastructure, and real metrics — no credit card required. Upgrade only when you outgrow it.

Early access spots are limited — join the waitlist today