The most common production failures. Even senior developers make these mistakes.
Every single one of these disasters has happened in production to real Laravel applications. Some caused hours of downtime. Some cost companies thousands of dollars. Read them. Memorize them. Prevent them.
DB::enableQueryLog() stores EVERY query in memory. After a few hours of production traffic, memory consumption grows until the server crashes with "Out of Memory".
// 💀 NEVER DO THIS IN PRODUCTION
DB::enableQueryLog();
$users = User::all();
$queries = DB::getQueryLog(); // This stays in memory forever!
Never use DB::enableQueryLog() in production. Use Laravel Debugbar in development, or Telescope for production monitoring.
// Only in local environment
if (app()->environment('local')) {
DB::enableQueryLog();
}
After running php artisan config:cache, the .env file is no longer loaded. Any call to env() outside config/*.php returns null. Database connections fail. The app crashes.
// 💀 In a controller or model - CRASHES after config:cache
$apiKey = env('STRIPE_KEY');
// 💀 In migration - CRASHES
$database = env('DB_DATABASE');
Never use env() outside config/*.php. Use config() everywhere else.
// ✅ CORRECT - Works before AND after cache
$apiKey = config('services.stripe.key');
User::all() loads ALL rows into memory. With 50,000 users, that's ~500MB of RAM. With 500,000 users, the server crashes with Out of Memory.
// 💀 Exporting 100k users - CRASHES
$users = User::all(); // 500MB+ memory
foreach ($users as $user) {
// process
}
Use lazy(), chunk(), or cursor() to stream data.
// ✅ Memory stays constant (~5MB)
foreach (User::cursor() as $user) {
// process one user at a time
}
Dispatching a full Model serializes ALL attributes (and relations!). Payload size can be 100KB-10MB per job. 10,000 jobs = 1GB+ in Redis. Queue stops. Server memory exhausted.
// 💀 DO NOT DO THIS
ProcessUserJob::dispatch($user); // Serializes entire user + relations!
Dispatch only the ID. Fetch fresh data inside the job.
// ✅ Dispatch ID only (8 bytes)
ProcessUserJob::dispatch($user->id);
// Inside job
$user = User::find($this->userId);
Without optimization, Laravel reads all config files, parses all routes, and discovers all events on EVERY request. 30-50ms of wasted CPU per request.
// 💀 Missing from deployment script
# Deploy without optimize
git pull
composer install
php artisan migrate
Always run php artisan optimize after deployment.
# Deployment script
git pull
composer install --no-dev --optimize-autoloader
php artisan migrate --force
php artisan optimize # ← CRITICAL!
You eager loaded posts, but forgot to eager load profile or comments. The view triggers N+1 queries silently. Works in development (small data), kills production (10,000+ rows).
// 💀 Controller
$users = User::with('posts')->get();
// 💀 View - N+1 for 'profile' relation
@foreach ($users as $user)
{{ $user->profile->bio }} // Extra query per user!
@endforeach
Enable preventLazyLoading() in development to catch these bugs.
// AppServiceProvider.php
Model::preventLazyLoading(!$this->app->isProduction());
// Now Laravel throws an exception if you forget eager loading!
Each Log::info() opens a file, writes, and closes it. With 10,000 iterations, that's 10,000 file I/O operations. Server I/O becomes the bottleneck.
// 💀 10,000 file writes
foreach ($users as $user) {
Log::info("Processing user {$user->id}");
$user->process();
}
Batch your logs or use memory logging.
// ✅ One file write
$logs = [];
foreach ($users as $user) {
$logs[] = "Processing user {$user->id}";
$user->process();
}
Log::info(implode("\n", $logs));
File-based sessions use file locking. When 100 concurrent users request the same session file, they wait in line. Response times skyrocket. Server crashes.
# .env - DEFAULT (DANGEROUS for production)
SESSION_DRIVER=file # ← File locking hell!
Use Redis for sessions (and cache).
# .env - PRODUCTION READY
SESSION_DRIVER=redis
CACHE_DRIVER=redis
A WHERE email = '...' clause without an index causes a full table scan. With 1M users, the query takes 5 seconds instead of 0.001 seconds.
-- 💀 No index = Full table scan (5 seconds)
SELECT * FROM users WHERE email = 'user@example.com';
Add indexes to all WHERE, JOIN, and ORDER BY columns. Run EXPLAIN to verify.
-- ✅ With index (0.001 seconds)
CREATE INDEX idx_users_email ON users(email);
-- Verify with EXPLAIN
EXPLAIN SELECT * FROM users WHERE email = 'user@example.com';
-- type should be 'ref' or 'range', NOT 'ALL'
Default setting pm.max_requests = 0 means PHP processes live forever. Memory leaks accumulate over time. After 3-7 days, each process consumes 500MB+ RAM. Server crashes. Restart fixes temporarily, then repeats.
# php-fpm.conf - DEFAULT (DANGEROUS)
pm.max_requests = 0 # ← Memory leak accumulator!
Set pm.max_requests to 500-1000. Processes restart after handling that many requests, freeing any leaked memory.
# php-fpm.conf - PRODUCTION READY
pm.max_requests = 500
pm.max_children = (Total RAM - Other services) / 30MB
grep -r "env(" --exclude-dir=vendor --exclude=config::dispatch() calls.Topic 16: PHP-FPM Deep Dive — Understanding pm.max_children, pm.start_servers, pm.min_spare_servers, pm.max_spare_servers, pm.max_requests, and how to calculate the right values for your server.