laravel-tornado maintained by ehab-talaat
πͺοΈ Laravel Tornado
HTTP stress testing for Laravel β built to feel native.
Test any endpoint directly from Artisan. Live request feed, full stats, smart body generation, auth support, multi-step sequences, and beautiful HTML reports.
Table of Contents
Installation
composer require ehab-talaat/laravel-tornado --dev
Laravel auto-discovers the package. No config file needed.
Requirements: PHP 8.1+, Laravel 10/11/12, Guzzle 7+
Quick Start
# Stress test an endpoint in 30 seconds
php artisan tornado api/users --method=POST --requests=500 --concurrency=25
Commands
tornado β Stress test a single endpoint
php artisan tornado {url} [options]
Examples
# ββ Basic GET ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
# 100 requests, 10 concurrent (defaults)
php artisan tornado api/products
# 1000 requests, 50 at a time
php artisan tornado api/products --requests=1000 --concurrency=50
# ββ POST with body βββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
php artisan tornado api/users \
--method=POST \
--requests=500 \
--concurrency=25 \
--body='{"name":"John","email":"john@test.com","password":"secret123"}'
# ββ With headers βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
php artisan tornado api/orders \
--method=POST \
--requests=200 \
--header="Authorization: Bearer YOUR_TOKEN" \
--header="Accept: application/json" \
--body='{"product_id":1,"qty":2}'
# ββ External URL βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
php artisan tornado https://api.example.com/v1/products --requests=300
# ββ Summary only (hide live feed) ββββββββββββββββββββββββββββββββββββββββββ
php artisan tornado api/products --requests=1000 --no-live
# ββ Interactive mode (no arguments needed) βββββββββββββββββββββββββββββββββ
php artisan tornado
tornado:sequence β Multi-step user flow
php artisan tornado:sequence {file?} [options]
Examples
# From a JSON file
php artisan tornado:sequence sequence.json --flows=50 --concurrency=10
# Interactive β Tornado asks you step by step
php artisan tornado:sequence
# With HTML export for each step
php artisan tornado:sequence sequence.json --flows=100 --export=html
Options Reference
tornado options
| Option | Default | Description |
|---|---|---|
url |
(required) | Relative /api/x or absolute https://β¦ |
--method |
GET |
HTTP method: GET, POST, PUT, PATCH, DELETE |
--requests |
100 |
Total number of requests |
--concurrency |
10 |
Simultaneous requests |
--timeout |
10 |
Per-request timeout in seconds |
--header |
β | Repeatable: --header="Key: Value" |
--body |
β | Raw JSON string for POST/PUT |
--smart |
β | Auto-generate body from validation rules |
--auth |
β | Auth driver: sanctum or passport |
--export |
β | Export format: json or html |
--no-live |
β | Skip live feed, show summary only |
tornado:sequence options
| Option | Default | Description |
|---|---|---|
file |
(optional) | Path to JSON sequence file |
--flows |
10 |
How many full user flows to simulate |
--concurrency |
5 |
Concurrent flows at a time |
--export |
β | Export each step: json or html |
Features
π― Interactive Mode
Run php artisan tornado with no arguments and Tornado asks you everything:
πͺοΈ Laravel Tornado
Endpoint (e.g. api/users): api/users
Method:
[0] GET
> [1] POST
[2] PUT
[3] PATCH
[4] DELETE
Total requests [100]: 1000
Concurrency (simultaneous) [10]: 50
Auto-generate body with --smart? (yes/no) [no]: yes
Is this route authenticated? (yes/no) [no]: yes
π§ Smart Mode β Auto-generate request body
Tornado reads your controller's validation rules and generates realistic fake data automatically using Faker. No need to write --body manually.
Supported validation styles:
// β
Array syntax
$request->validate([
'name' => ['required', 'string', 'max:255'],
'email' => ['required', 'email'],
'password' => ['required', 'string', 'min:8'],
]);
// β
String syntax
$request->validate([
'name' => 'required|string|max:255',
'email' => 'required|email',
]);
// β
FormRequest class
class StoreUserRequest extends FormRequest
{
public function rules(): array
{
return [
'name' => ['required', 'string'],
'email' => ['required', 'email'],
];
}
}
Usage:
php artisan tornado api/users --method=POST --requests=500 --smart
What Tornado generates per rule/field:
| Rule / Field name | Generated value |
|---|---|
email rule |
fake()->safeEmail() |
url rule |
fake()->url() |
integer / numeric |
fake()->numberBetween(min, max) |
boolean |
fake()->boolean() |
date |
fake()->date() |
uuid |
fake()->uuid() |
field contains name |
fake()->name() |
field contains password |
Password@123 format |
field contains phone |
fake()->phoneNumber() |
field contains address |
fake()->address() |
field contains city |
fake()->city() |
field contains price / amount |
fake()->randomFloat(2, 1, 999) |
field ends with _id |
fake()->numberBetween(1, 100) |
A fresh fake value is generated for every single request.
π Auth Mode β Authenticated routes
Tornado logs in once, gets the token, and injects it into all requests automatically.
Interactive:
php artisan tornado api/orders --method=POST --requests=500
Is this route authenticated? yes
Auth driver:
> sanctum
Login field:
> email β or username, phone, or type any custom field
email: admin@test.com
Password: ********
Logging in...
β Token obtained successfully.
Direct (skip prompts):
php artisan tornado api/orders --method=POST --requests=500 --auth=sanctum
Supported drivers:
| Driver | Login endpoints tried |
|---|---|
sanctum |
/api/login, /login, /api/auth/login, /api/sanctum/token |
passport |
/oauth/token, /api/oauth/token |
Supported response shapes (any of these work out of the box):
{ "token": "abc123" }
{ "access_token": "abc123" }
{ "data": { "token": "abc123" } }
{ "data": { "access_token": "abc123" } }
{ "authorisation": { "token": "abc123" } }
π Export Reports
Save results for sharing or comparing over time.
# JSON export
php artisan tornado api/users --requests=1000 --export=json
# HTML export (beautiful report with charts)
php artisan tornado api/users --requests=1000 --export=html
Files are saved to storage/tornado/ with a timestamp:
storage/tornado/tornado_api-users_2024-01-15_14-30-22.html
storage/tornado/tornado_api-users_2024-01-15_14-30-22.json
JSON report structure:
{
"meta": {
"generated_at": "2024-01-15T14:30:22+00:00",
"package": "laravel-tornado"
},
"scenario": {
"url": "http://localhost/api/users",
"method": "POST",
"requests": 1000,
"concurrency": 50
},
"results": {
"total": 1000,
"success": 994,
"failed": 6,
"success_pct": 99.4,
"failed_pct": 0.6,
"duration_sec": 12.4,
"rps": 80.6,
"status_codes": { "200": 994, "500": 6 }
},
"response_times": {
"min": 31.0,
"avg": 142.3,
"p50": 128.0,
"p75": 210.5,
"p90": 310.2,
"p95": 380.0,
"p99": 610.4,
"max": 891.0
}
}
HTML report includes:
- Stat cards (total, RPS, success/fail, avg, P95)
- Latency distribution bar chart
- Status codes doughnut chart
- Full percentile table (min β max)
- Dark theme, self-contained single file
π Sequence Mode β Multi-step flows
Simulate a real user flow: login β browse β create β logout. Each step passes data to the next via extract.
JSON file format
{
"steps": [
{
"name": "Login",
"method": "POST",
"url": "api/login",
"body": {
"email": "admin@test.com",
"password": "password"
},
"extract": {
"token": "data.token",
"user_id": "data.user.id"
}
},
{
"name": "Get Profile",
"method": "GET",
"url": "api/profile",
"headers": {
"Authorization": "Bearer {token}"
}
},
{
"name": "Create Order",
"method": "POST",
"url": "api/orders",
"headers": {
"Authorization": "Bearer {token}"
},
"smart": true,
"requests": 200,
"concurrency": 20
},
{
"name": "List My Orders",
"method": "GET",
"url": "api/users/{user_id}/orders",
"headers": {
"Authorization": "Bearer {token}"
}
},
{
"name": "Logout",
"method": "POST",
"url": "api/logout",
"headers": {
"Authorization": "Bearer {token}"
}
}
]
}
Run it:
php artisan tornado:sequence sequence.json --flows=100 --concurrency=10 --export=html
How extract works
Use dot-notation to pull any value from a JSON response:
"extract": {
"token": "data.token",
"user_id": "data.user.id",
"company_id": "data.user.company.id"
}
Then use {token}, {user_id}, {company_id} as placeholders in any later step's URL, headers, or body:
"url": "api/companies/{company_id}/users/{user_id}"
"headers": { "Authorization": "Bearer {token}" }
"body": { "owner_id": "{user_id}" }
Step-level options
Override load settings per step:
{
"name": "Heavy endpoint",
"url": "api/reports/generate",
"method": "POST",
"requests": 500,
"concurrency": 25
}
If omitted, the step inherits --flows and --concurrency from the command.
Interactive sequence
php artisan tornado:sequence
Define your steps (press Enter with empty URL to finish)
ββ Step 1 ββ
URL (e.g. api/login): api/login
Step name [api/login]: Login
Method: POST
Auto-generate body with --smart? no
JSON body: {"email":"admin@test.com","password":"secret"}
Add headers? no
Extract values from response? yes
Extract (leave empty to stop): token=data.token
Extract (leave empty to stop): [Enter]
ββ Step 2 ββ
URL: api/orders
...
Reading the Output
Live feed
[ 1/1000] [ββββββββββββββββββββ] 200 43.2ms
[ 2/1000] [ββββββββββββββββββββ] 200 38.7ms
[ 3/1000] [ββββββββββββββββββββ] 500 210.1ms β Internal Server Error
Final summary
βββββββββββββββββββββββββββββββββββββββββββββββββββββββ
πͺοΈ Tornado Report
βββββββββββββββββββββββββββββββββββββββββββββββββββββββ
Total : 1000 requests
Duration : 12.4s
Throughput : 80.6 req/sec
β
Success : 994 (99.4%)
β Failed : 6 (0.6%)
Status codes:
200 β β β β β β β β β β β β β β β β β β β β β β β β β β β β β 994 (99.4%)
500 β 6 (0.6%)
Response times (successful requests):
Min : 31.0ms
Avg : 142.3ms
P50 : 128.0ms
P75 : 210.5ms
P90 : 310.2ms
P95 : 380.0ms β 95% of requests finished within this
P99 : 610.4ms β only 1% were slower than this
Max : 891.0ms
Latency dist : βββββ
βββββ
31ms 891ms
What the metrics mean
| Metric | What it tells you |
|---|---|
| RPS | Requests per second your server handled |
| P50 | Half your users are faster than this |
| P95 | 95% of users are faster than this β your real-world experience |
| P99 | Your worst-case users |
| Failed % | High = DB overload, rate limiting, or crashes |
| 500 errors | Check storage/logs/laravel.log |
Performance benchmarks
| P95 response | Rating |
|---|---|
| < 100ms | π’ Excellent |
| 100β300ms | π‘ Good |
| 300β800ms | π Needs work |
| > 800ms | π΄ Critical |
License
MIT β Ehab Talaat