We earn commissions when you shop through the links below.
If you’ve shipped a Laravel app to production and later found yourself debugging a mystery performance issue at 2am, you already know the gap between “it works locally” and “it works in production” can be brutal. Following Laravel best practices for production apps isn’t about being dogmatic — it’s about avoiding the predictable failures before they cost you users and sleep. This guide covers the areas that matter most: configuration hardening, performance, queues, error handling, and deployment workflows.
1. Lock Down Your Environment Configuration
Your .env file is not a place to be sloppy. In production, make sure these are set correctly:
APP_ENV=production
APP_DEBUG=false
APP_KEY=base64:your-generated-key-here
LOG_CHANNEL=stack
SESSION_DRIVER=redis
CACHE_DRIVER=redis
QUEUE_CONNECTION=redis
Setting APP_DEBUG=false is non-negotiable. A debug page in production leaks stack traces, environment variables, and database credentials to anyone who triggers an exception. Use Redis for session, cache, and queue drivers rather than the database driver — it dramatically reduces write pressure on your DB and is far more performant for these workloads.
Never commit your .env file to version control. Use a secrets manager or your hosting platform’s environment variable injection instead.
2. Run Artisan Optimize on Every Deploy
Laravel loads a lot of configuration, routes, and views at runtime unless you tell it not to. Before each deploy, run:
php artisan config:cache
php artisan route:cache
php artisan view:cache
php artisan event:cache
Or just use the single command added in recent Laravel versions:
php artisan optimize
This collapses all configuration files into a single cached file, compiles routes into a fast lookup table, and pre-renders Blade templates. On a busy app this alone can cut your average response time noticeably. Equally important: when you deploy new code, run php artisan optimize:clear first, then re-cache after.
3. Use Queues for Anything Non-Instant
Sending emails, processing uploads, hitting third-party APIs, generating reports — none of these belong in the request/response cycle. Move them to queued jobs:
// Instead of this in a controller:
Mail::to($user)->send(new WelcomeEmail($user));
// Do this:
Mail::to($user)->queue(new WelcomeEmail($user));
// Or dispatch a dedicated job:
SendWelcomeEmail::dispatch($user)->onQueue('emails');
In production, run your queue workers with Supervisor so they restart automatically on failure. A basic Supervisor config looks like this:
[program:laravel-worker]
process_name=%(program_name)s_%(process_num)02d
command=php /var/www/artisan queue:work redis --sleep=3 --tries=3 --max-time=3600
autostart=true
autorestart=true
stopasgroup=true
killasgroup=true
user=www-data
numprocs=4
redirect_stderr=true
stdout_logfile=/var/www/storage/logs/worker.log
stopwaitsecs=3600
If you’re deploying on Railway, you can run your queue worker as a separate service/process alongside your web service without needing to configure Supervisor manually — it handles the process management for you.
4. Handle Exceptions Properly
The default Laravel exception handler is fine for development. In production you need visibility into what’s breaking and where. Integrate a proper error tracking tool (Sentry, Flare, Bugsnag) and configure your bootstrap/app.php to report exceptions:
->withExceptions(function (Exceptions $exceptions) {
$exceptions->report(function (Throwable $e) {
if (app()->bound('sentry')) {
app('sentry')->captureException($e);
}
});
$exceptions->dontReport([
AuthenticationException::class,
ValidationException::class,
]);
})
Also make sure you have custom error views for 404, 500, and 503 responses — nothing undermines user trust like a raw Laravel error page or a blank white screen.
5. Protect Against N+1 Queries
This is one of the most common performance killers in Laravel apps. If you’re looping over a collection and calling a relationship inside the loop, you’re firing a query per iteration:
// Bad: N+1 problem
$posts = Post::all();
foreach ($posts as $post) {
echo $post->author->name; // Hits the DB every iteration
}
// Good: eager load
$posts = Post::with('author')->get();
foreach ($posts as $post) {
echo $post->author->name; // No extra queries
}
Enable strict mode in your AppServiceProvider during development so Laravel throws an exception when a lazy load is detected:
use Illuminate\Database\Eloquent\Model;
public function boot(): void
{
Model::preventLazyLoading(!app()->isProduction());
}
This catches N+1 issues before they reach production without blocking anything in the live environment.
6. Implement Proper Caching Strategies
Beyond route and config caching, use application-level caching for expensive queries and computed values:
$stats = Cache::remember('dashboard.stats', now()->addMinutes(15), function () {
return [
'users' => User::count(),
'revenue' => Order::sum('total'),
'active' => Subscription::active()->count(),
];
});
For things that change per-user, use cache tags to group and invalidate related entries together. And make sure your cache TTLs are actually thought through — caching something for 24 hours when the underlying data changes every 5 minutes is a bug, not a feature.
7. Secure Your Application
Security deserves its own post, but here are the non-negotiables for production:
- Always use parameterized queries — Eloquent handles this by default, but watch out for raw
DB::select()calls with interpolated user input. - Validate and authorize everything — Use Form Requests for validation and Gates/Policies for authorization. Never trust user input.
- Enforce HTTPS — Set
APP_URLwith https and force HTTPS redirects at the server or middleware level. - Rate limit sensitive routes — Use Laravel’s built-in throttle middleware on login, registration, and API endpoints.
- Rotate your APP_KEY carefully — Changing it invalidates all encrypted values including sessions and cookies.
// In routes/web.php or a middleware group
Route::middleware(['auth', 'throttle:api'])->group(function () {
Route::post('/api/transfer', [TransferController::class, 'store']);
});
8. Set Up a Proper Deployment Pipeline
Manual deployments via FTP or SSH are a liability. At minimum, automate your deploy steps in a script or CI/CD pipeline:
#!/bin/bash
set -e
git pull origin main
composer install --no-dev --optimize-autoloader
php artisan migrate --force
php artisan optimize:clear
php artisan optimize
php artisan queue:restart
The --force flag on migrations bypasses the production confirmation prompt, which is required in CI environments. The queue:restart command signals workers to finish their current job and restart, picking up your new code cleanly.
For hosting, DigitalOcean App Platform or Droplets are a solid choice for Laravel apps — you get full control over PHP versions, Nginx config, and can set up managed databases and Redis clusters that integrate cleanly with Laravel’s drivers.
9. Monitor Performance in Production
You can’t fix what you can’t see. Add query logging in development:
DB::listen(function ($query) {
if ($query->time > 100) {
Log::warning('Slow query detected', [
'sql' => $query->sql,
'time' => $query->time,
]);
}
});
In production, use Laravel Telescope (gated behind an auth check) or Laravel Pulse for a real-time dashboard of requests, jobs, exceptions, and slow queries. Pulse is lightweight enough to run in production without significant overhead.
Wrapping Up
The Laravel best practices for production apps covered here aren’t theoretical — they’re the things that separate apps that scale gracefully from apps that collapse under load or get pwned. Caching, queues, eager loading, environment hardening, and a solid deployment pipeline are your foundation. Build on top of that with proper monitoring and you’ll spend a lot less time firefighting.
If you’re still sharpening your Laravel fundamentals alongside these production habits, Udemy has deep-dive Laravel courses that cover architecture patterns and real-world application structure — worth having in your back pocket.
Ship with confidence. Your future self will thank you.