ehab-talaat/laravel-tornado

🌪️ HTTP stress testing for Laravel — live feed + full stats

Maintainers

Package info

github.com/ehabtalaat/laravel-tornado

pkg:composer/ehab-talaat/laravel-tornado

Transparency log

Statistics

Installs: 1

Dependents: 0

Suggesters: 0

Stars: 0

Open Issues: 0

v1.0.0 2026-07-24 14:23 UTC

This package is auto-updated.

Last update: 2026-07-24 14:30:05 UTC


README

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.

PHP Laravel License

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