Why every request runs 10 middleware classes (and why it shouldn't).
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.
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.
| Middleware | Typical Cost | What It Does | Needed 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) |
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.
Don't put middleware in global groups. Attach them only to the routes that actually need them.
// 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,
],
];
// 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']);
});
For each middleware in your web or api group, ask:
| Middleware | API 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) |
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.
// 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!
# 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 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.
'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.
'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.
| Strategy | Middleware per Request | Time Saved | Implementation |
|---|---|---|---|
| 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 |
withoutMiddleware() for public endpoints. Put fast middleware first. Your CPU will thank you.
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.