📁 Volume II: Laravel Performance Tuning Kit

🚪 Topic 8: Middleware Selectivity

Why every request runs 10 middleware classes (and why it shouldn't).

"Every middleware is a gatekeeper.
Your /health endpoint doesn't need authentication, session, or CSRF protection.
But Laravel runs them anyway. And you pay the price."
THE DEFAULT CONFIGURATION

Most Laravel apps have a Kernel.php that looks like this:

protected $middlewareGroups = [
    'web' => [
        \App\Http\Middleware\EncryptCookies::class,
        \Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse::class,
        \Illuminate\Session\Middleware\StartSession::class,
        \Illuminate\View\Middleware\ShareErrorsFromSession::class,
        \App\Http\Middleware\VerifyCsrfToken::class,
        \Illuminate\Routing\Middleware\SubstituteBindings::class,
    ],
    'api' => [
        \App\Http\Middleware\EncryptCookies::class,
        \Illuminate\Session\Middleware\StartSession::class,  // ← Why?
        \App\Http\Middleware\VerifyCsrfToken::class,          // ← For API?
        'throttle:api',
        \Illuminate\Routing\Middleware\SubstituteBindings::class,
    ],
];

Every single request that hits your API runs through all these middleware classes — even public endpoints that don't need them.

🔴 The Problem: Middleware Overhead

What happens when a request passes through middleware:

│ │ REQUEST JOURNEY THROUGH MIDDLEWARE │ ═══════════════════════════════════════════════════════════════════ │ │ HTTP Request arrives │ │ │ ▼ │ ┌─────────────────────────────────────────────────────────────┐ │ │ Middleware 1: EncryptCookies │ │ │ → Decrypts cookie payload (CPU + I/O) │ │ │ → Time: ~0.5ms │ │ └─────────────────────────────────────────────────────────────┘ │ │ │ ▼ │ ┌─────────────────────────────────────────────────────────────┐ │ │ Middleware 2: StartSession │ │ │ → Reads session from file/redis (I/O) │ │ │ → Deserializes session data (CPU) │ │ │ → Time: ~2-5ms (file) or ~0.5ms (redis) │ │ └─────────────────────────────────────────────────────────────┘ │ │ │ ▼ │ ┌─────────────────────────────────────────────────────────────┐ │ │ Middleware 3: VerifyCsrfToken │ │ │ → Extracts CSRF token from request │ │ │ → Compares with session token │ │ │ → Time: ~0.5ms │ │ └─────────────────────────────────────────────────────────────┘ │ │ │ ▼ │ ┌─────────────────────────────────────────────────────────────┐ │ │ Middleware 4: SubstituteBindings │ │ │ → Replaces {id} with actual model (DB query) │ │ │ → Time: ~1-5ms (if DB query) │ │ └─────────────────────────────────────────────────────────────┘ │ │ │ ▼ │ ┌─────────────────────────────────────────────────────────────┐ │ │ Middleware 5: ThrottleRequests (rate limiter) │ │ │ → Checks cache for request count │ │ │ → Time: ~0.5ms │ │ └─────────────────────────────────────────────────────────────┘ │ │ │ ▼ │ Controller executes (finally!) │
TOTAL OVERHEAD

5-15ms of pure middleware overhead per request — before your controller even runs. For a public API endpoint that just returns {status: "ok"}, this is 100% wasted CPU.

📊 Cost Breakdown per Middleware

MiddlewareTypical CostWhat It DoesNeeded for Public API?
EncryptCookies 0.3-0.5ms Decrypts cookie payload ❌ No (unless using cookies)
StartSession 2-5ms (file) / 0.5ms (redis) Loads session from storage ❌ No (stateless API)
VerifyCsrfToken 0.3-0.5ms Validates CSRF token ❌ No (use API tokens)
SubstituteBindings 1-5ms (may query DB) Route model binding ✅ Yes (often needed)
ThrottleRequests 0.3-0.5ms Rate limiting ✅ Yes (security)
THE MATH

If your API has 1,000,000 requests per day, and you remove 4 unnecessary middleware (saving 3ms each):

1,000,000 × 12ms = 12,000 seconds = 3.3 hours of CPU saved daily.

That's real money on cloud infrastructure.

✅ The Fix: Specialized Middleware Groups

THE GOLDEN RULE OF MIDDLEWARE

Don't put middleware in global groups. Attach them only to the routes that actually need them.

Step 1: Refactor Kernel.php

// app/Http/Kernel.php
protected $middlewareGroups = [
    // Web group - minimal!
    'web' => [
        \Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse::class,
        \Illuminate\Routing\Middleware\SubstituteBindings::class,
    ],
    
    // API group - minimal!
    'api' => [
        \Illuminate\Routing\Middleware\SubstituteBindings::class,
        'throttle:api',
    ],
    
    // NEW: Public endpoints (health, status, ping)
    'public' => [
        // Absolutely nothing!
        // No session, no cookies, no CSRF, no auth
    ],
    
    // NEW: Authenticated API
    'auth-api' => [
        'auth:sanctum',
        \Illuminate\Routing\Middleware\SubstituteBindings::class,
        'throttle:api',
    ],
    
    // NEW: Web with session (login pages, etc.)
    'web-session' => [
        \App\Http\Middleware\EncryptCookies::class,
        \Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse::class,
        \Illuminate\Session\Middleware\StartSession::class,
        \Illuminate\View\Middleware\ShareErrorsFromSession::class,
        \App\Http\Middleware\VerifyCsrfToken::class,
        \Illuminate\Routing\Middleware\SubstituteBindings::class,
    ],
];

Step 2: Apply middleware groups to routes

// routes/api.php

// Public health check — NO MIDDLEWARE
Route::get('/health', [HealthController::class, 'check'])
    ->withoutMiddleware(['api']);  // Or use 'public' group

// Public login — minimal middleware
Route::post('/login', [AuthController::class, 'login'])
    ->middleware('throttle:10,1');  // Only rate limiting

// Authenticated endpoints — full auth middleware
Route::middleware(['auth:sanctum', 'throttle:api'])->group(function () {
    Route::get('/user', [UserController::class, 'show']);
    Route::post('/orders', [OrderController::class, 'store']);
});

// Admin endpoints — even more middleware
Route::middleware(['auth:sanctum', 'admin', 'throttle:admin'])->group(function () {
    Route::get('/admin/users', [AdminController::class, 'users']);
    Route::delete('/admin/users/{id}', [AdminController::class, 'deleteUser']);
});

🎨 Before vs After (Visualized)

👎 BEFORE (All middleware, all requests)

GET /api/health │ ├── EncryptCookies (0.5ms) ├── StartSession (2ms) ├── VerifyCsrfToken (0.5ms) ├── SubstituteBindings (1ms) ├── ThrottleRequests (0.5ms) │ └── Controller (0.1ms) TOTAL: ~4.6ms for a health check!

👍 AFTER (Only what's needed)

GET /api/health │ └── Controller (0.1ms) TOTAL: ~0.1ms for a health check! 46x faster!

🔍 How to Identify Unnecessary Middleware

AUDIT EACH MIDDLEWARE

For each middleware in your web or api group, ask:

  1. Does my API need sessions? → No → Remove StartSession
  2. Does my API use cookies? → No → Remove EncryptCookies
  3. Is this a public endpoint? → Yes → Remove auth middleware
  4. Is this a GET request? → Yes → CSRF only needed for POST/PUT/DELETE
  5. Does this route use route model binding? → No → Remove SubstituteBindings

Common middleware and their necessity:

MiddlewareAPI Need?Web Need?Public Endpoint Need?
EncryptCookies ❌ No ✅ Yes ❌ No
StartSession ❌ No (stateless API) ✅ Yes ❌ No
VerifyCsrfToken ❌ No (use tokens) ✅ Yes ❌ No
SubstituteBindings ✅ Yes (if using route model binding) ✅ Yes ❌ No (if no {id} in URL)
ThrottleRequests ✅ Yes (rate limiting) ✅ Yes ✅ Yes (security)

🏥 The Health Check: A Case Study

THE MOST ABUSED ENDPOINT

Every load balancer, every monitoring system, every Kubernetes liveness probe calls your /health endpoint — sometimes every 5 seconds.

If your health check runs through 10 middleware, you're wasting massive CPU on nothing.

THE FIX
// routes/web.php or routes/api.php
Route::get('/health', function () {
    return response()->json([
        'status' => 'healthy',
        'timestamp' => now(),
    ]);
})->withoutMiddleware([\App\Http\Middleware\VerifyCsrfToken::class])
  ->withoutMiddleware([\Illuminate\Session\Middleware\StartSession::class])
  ->withoutMiddleware([\App\Http\Middleware\EncryptCookies::class]);

// Or even better — serve health check directly from Nginx,
// bypassing PHP entirely!
ULTIMATE HEALTH CHECK — NO PHP!
# Nginx configuration
location /health {
    access_log off;
    return 200 "healthy\n";
    add_header Content-Type text/plain;
}

# Your PHP application never even wakes up!

Result: 0ms response time for health checks. Your load balancer is happy. Your PHP-FPM processes are free for real users.

📋 Middleware Order Matters (A Lot)

THE OPTIMIZATION OPPORTUNITY

Middleware runs in the order they're listed. If a request is going to fail (e.g., invalid auth token), you want it to fail FAST.

👎 BAD ORDER (Expensive first)

'api' => [
    \Illuminate\Session\Middleware\StartSession::class,  // 2-5ms
    \App\Http\Middleware\EncryptCookies::class,          // 0.5ms
    \App\Http\Middleware\VerifyCsrfToken::class,        // 0.5ms
    \App\Http\Middleware\Authenticate::class,           // 1ms (but fails!)
]

If auth fails, you still paid for session + cookies + CSRF: ~4ms wasted per failed request.

👍 GOOD ORDER (Cheap and fast first)

'api' => [
    \App\Http\Middleware\Authenticate::class,           // 1ms (fails fast!)
    \App\Http\Middleware\ThrottleRequests::class,       // 0.5ms
    \Illuminate\Session\Middleware\StartSession::class, // Only if auth passes
    \App\Http\Middleware\EncryptCookies::class,
    \App\Http\Middleware\VerifyCsrfToken::class,
]

If auth fails, you pay only 1ms before rejection.

🔴 THE RULE OF MIDDLEWARE ORDER:
Put cheap, fast-failing middleware first. Put expensive, rarely-failing middleware last.

📝 Topic 8 Summary: Middleware Selectivity

StrategyMiddleware per RequestTime SavedImplementation
Default (all middleware everywhere) 8-12 0ms (baseline) Just use web/api groups
Remove unnecessary middleware 3-5 5-10ms Audit each middleware
Specialized groups + public group 0-3 10-15ms Create 'public', 'auth-api', 'web-session' groups
Health check via Nginx (no PHP) 0 100% of health check time Nginx returns 200 directly
📌 THE RULE: Every middleware is a tax on every request that goes through it. If a route doesn't need a feature, don't make it pay the tax.

Create specialized middleware groups. Use withoutMiddleware() for public endpoints. Put fast middleware first. Your CPU will thank you.
NEXT TOPIC PREVIEW

Topic 9: Database Indexing & EXPLAIN — The #1 database performance killer. Why missing indexes cause full table scans. How to read EXPLAIN output. The difference between ref, range, and ALL.