Looking to hire Laravel developers? Try LaraJobs

laravel-tornado maintained by ehab-talaat

Description
πŸŒͺ️ HTTP stress testing for Laravel β€” live feed + full stats
Last update
2026/07/24 16:23 (dev-main)
License
Links
Downloads
1

Comments
comments powered by Disqus

πŸŒͺ️ 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.

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