Laravel Route Caching: When It Helps, When It Breaks, and How to Deploy It Safely
Laravel route caching cuts route registration overhead, but closure routes break it and skipped rebuilds serve stale routes. Here's how to adopt it safely.
22 May 2026, 09:26 UTC

Your Laravel app boots fast locally, but in production every request re-parses hundreds of lines of route definitions before a single controller runs. Route caching eliminates that repeated work — but only if your routes are cacheable, and only if your deploy process rebuilds the cache every time. Get either part wrong and you'll serve stale routes or crash the deploy.
What route caching actually does
On a normal request, Laravel loads every file in routes/, evaluates each Route:: call, and builds the route collection from scratch. With route caching enabled, php artisan route:cache serializes the fully-built route collection into a single PHP file under bootstrap/cache/. Subsequent requests load that one file instead of parsing your route definitions again.
The important nuance: this only speeds up route registration. Route model binding, parameter constraints, middleware execution, and controller logic all run exactly as before. If your app has 30 routes, you will not feel the difference. If it has 800 routes across several files — common in larger APIs and modular monoliths — registration time drops substantially, and that's measurable on every single request.
The closure trap
The one hard rule: every cached route must resolve to a controller method or an invokable class. Closure routes cannot be serialized, and running route:cache with even one closure route fails with an error about serializing closures. This catches people because quick utility routes tend to be closures:
// routes/web.php — this breaks route:cache
Route::get('/ping', fn () => response()->json(['ok' => true]));
// cacheable equivalent
Route::get('/ping', PingController::class);
<?php
// app/Http/Controllers/PingController.php
namespace App\Http\Controllers;
use Illuminate\Http\JsonResponse;
class PingController extends Controller
{
public function __invoke(): JsonResponse
{
return response()->json(['ok' => true]);
}
}
An invokable controller (a class with a single __invoke method) is the cleanest fix for one-off endpoints — no route definition changes beyond swapping the closure for the class name.
The real risk: stale routes in production
The cache is not self-invalidating. Once bootstrap/cache/routes-*.php exists, Laravel uses it and ignores your routes/ files entirely. Add a route, deploy, forget to rebuild — and the new endpoint 404s in production while working perfectly on your uncached local machine. This is the failure mode that actually bites teams, and it's why route caching belongs in your deploy script, not run by hand:
# Run on the production server as the deploy user, after code is updated
php artisan config:clear
php artisan route:clear # drop the old cache first
php artisan route:cache # rebuild from current code
Run these from your application's root directory with a user that has write access to bootstrap/cache/. Clearing before rebuilding avoids serving a half-updated cache during the swap. Note that route:clear briefly leaves the app uncached — harmless, just slightly slower for the requests in that window.
Verifying it worked
Don't assume success from a quiet deploy. Three quick checks:
php artisan route:cacheprints a success message; a closure route anywhere in the app makes it fail loudly, which is itself a useful CI check.php artisan route:listafter caching shows your full route table, confirming the cache was built from current definitions.- Add a trivial route, rebuild the cache, and confirm it appears in
route:list— this proves your deploy pipeline actually refreshes the cache rather than shipping a stale file.
If you want to quantify the benefit, hit a simple endpoint before and after caching with a profiler or repeated timing requests. You should see reduced bootstrapping time; if you don't, your route count is probably too small for caching to matter.
The trade-off
Route caching trades flexibility for boot speed. You give up closure routes, you add a mandatory step to every deploy, and you create a new class of "works locally, 404s in production" bugs if the rebuild is skipped. For small apps, that trade isn't worth it. For route-heavy applications — especially APIs with hundreds of endpoints — it's one of the cheapest performance wins available, and the closure restriction doubles as a nudge toward cleaner, testable controllers.
Actionable closing
Try it safely: run php artisan route:cache in staging first. If it errors, convert the offending closures to invokable controllers. If it succeeds, add the clear-then-cache pair to your deploy script, then verify with route:list on the next deploy. Behavior described here applies to current Laravel versions (10/11 and later); confirm the exact cache file path and command output against your version's upgrade notes.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.