Laravel Production Nightmare: Why My Queue Workers Keep Stuck on MySQL Timeout in Docker + Nginx – Fixing the Fatal 500 Crash in 30 Minutes
You’ve just pushed a hot‑fix to production, the API spikes, and suddenly all your Laravel queue workers die with a 500 error. The logs scream “MySQL server has gone away” and the Docker containers are stuck in a restart loop. Sound familiar? You’re not alone – the same fatal 500 crash has haunted countless PHP optimization teams on VPS, shared hosting, and even managed WordPress‑Laravel hybrids.
Why This Matters
Queue workers are the heartbeat of any modern SaaS or high‑traffic WordPress site that uses Laravel as a micro‑service. When they freeze, emails stop, notifications disappear, and revenue pipelines choke. A MySQL timeout inside Docker often means you’re burning CPU cycles, paying for over‑provisioned VPS plans, and losing customer trust – all avoidable with a few tuned settings.
Common Causes
- Default MySQL
wait_timeout(8 seconds) collides with long‑running jobs. - Improper
php-fpmpm.max_childrencausing worker starvation. - Docker networking latency between the app container and the MySQL service.
- Supervisor not restarting failed workers fast enough.
- Missing Redis queue driver fallback.
Step‑By‑Step Fix Tutorial
1️⃣ Extend MySQL Timeout Inside Docker
Override the MySQL configuration with a custom my.cnf and mount it.
# docker-compose.yml (excerpt)
services:
mysql:
image: mysql:8.0
restart: unless-stopped
environment:
MYSQL_ROOT_PASSWORD: secret
MYSQL_DATABASE: app
volumes:
- ./docker/mysql/conf.d:/etc/mysql/conf.d
- mysql_data:/var/lib/mysql
command: --default-authentication-plugin=mysql_native_password
# ./docker/mysql/conf.d/custom.cnf
[mysqld]
wait_timeout = 28800
interactive_timeout = 28800
max_allowed_packet = 64M
Re‑create the container:
docker-compose down -v && docker-compose up -d
2️⃣ Optimize PHP‑FPM Pool
Increase the pm.max_children to match your VPS CPU count and set a reasonable request_terminate_timeout.
# ./docker/php-fpm/pool.d/www.conf
[www]
user = www-data
group = www-data
listen = /run/php/php-fpm.sock
pm = dynamic
pm.max_children = 20 ; 2‑3 per CPU core
pm.start_servers = 5
pm.min_spare_servers = 5
pm.max_spare_servers = 12
request_terminate_timeout = 300
3️⃣ Nginx FastCGI Timeout Tweaks
Make sure Nginx gives PHP enough time to finish the job.
# /etc/nginx/conf.d/laravel.conf
server {
listen 80;
server_name app.example.com;
root /var/www/html/public;
index index.php;
location / {
try_files $uri $uri/ /index.php?$query_string;
}
location ~ \.php$ {
fastcgi_pass unix:/run/php/php-fpm.sock;
fastcgi_read_timeout 300;
fastcgi_send_timeout 300;
include fastcgi_params;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
}
}
4️⃣ Supervisor Config for Queue Workers
Force a quick restart when a worker exits with code 1 (MySQL timeout).
# ./docker/supervisor/conf.d/laravel-worker.conf
[program:laravel-worker]
process_name=%(program_name)s_%(process_num)02d
command=php /var/www/html/artisan queue:work redis --sleep=3 --tries=3 --queue=default
autostart=true
autorestart=true
stopwaitsecs=360
numprocs=4
user=www-data
stdout_logfile=/var/log/worker.log
stderr_logfile=/var/log/worker_error.log
exitcodes=0,2
5️⃣ Verify Redis Connectivity
Check that the REDIS_HOST env variable points to the Docker network alias, not 127.0.0.1.
# .env
QUEUE_CONNECTION=redis
REDIS_HOST=redis
REDIS_PORT=6379
6️⃣ Run Composer Optimizations
After fixing the environment, clear caches and optimize the autoloader.
docker exec -it app php artisan cache:clear
docker exec -it app php artisan config:cache
docker exec -it app composer install --optimize-autoloader --no-dev
php artisan horizon instead of the basic worker if you already use Laravel Horizon – it ships with built‑in health checks and auto‑restart.
VPS or Shared Hosting Optimization Tips
- Allocate at least 2 GB RAM for MySQL on a VPS; enable
innodb_buffer_pool_size=1G. - On shared hosting, switch the queue driver to
databaseand runphp artisan queue:work --daemonvia cron every minute. - Enable
opcache.enable=1andopcache.memory_consumption=256inphp.ini. - Use Cloudflare “Always Online” page rules to keep static assets cached while workers restart.
Real World Production Example
Acme SaaS migrated from a 2‑core DigitalOcean droplet to a 4‑core Linode. Before the fix:
- Average job duration: 2 seconds
- Queue time: 45 seconds (timeout every 10 minutes)
- CPU spike: 95 % during traffic bursts
After applying the steps above:
- Job latency dropped to 0.8 seconds
- CPU avg: 30 %
- No more 500 crashes for 30 days straight.
Before vs After Results
| Metric | Before | After |
|---|---|---|
| MySQL timeout errors | 34/hr | 0 |
| Queue latency | 45 s | 0.8 s |
| CPU usage | 95 % | 30 % |
Security Considerations
Never expose MySQL ports to the public internet. Use Docker’s internal network or a VPN tunnel. RotateAPP_KEYandDB_PASSWORDafter each major deployment, and enablesslmode=requireon the MySQL client if you run on a cloud provider.
Bonus Performance Tips
- Enable
redis-cli CONFIG SET maxmemory 256mbandmaxmemory-policy allkeys-lrufor automatic eviction. - Place
opcache.validate_timestamp=0on production – reload opcache only on deploy. - Use
php artisan route:cacheandphp artisan view:cacheafter each code push. - Consider cheap secure hosting with built‑in Varnish if Docker overhead is too much for a small WordPress‑Laravel hybrid.
FAQ
Q: My queue still restarts after the fix. What next?
A: Check supervisorctl tail laravel-worker for hidden PHP fatal errors. Often a missing php-mysql extension in the container will re‑trigger a timeout.
Q: Can I run the same setup on a shared cPanel host?
A: Yes, but replace Docker with separate cron jobs and use the host’s MySQL service. Increase max_execution_time in php.ini to at least 300 seconds.
Q: Do I need to restart Nginx after each deploy?
A: Not always. Reload is enough: nginx -s reload. Full restart can cause a tiny downtime spike on high‑traffic sites.
Final Thoughts
Queue workers hanging on MySQL timeouts isn’t a mystical Laravel bug – it’s a classic mismatch between container defaults and real‑world production load. By extending MySQL timeouts, tuning PHP‑FPM, adjusting Nginx fastcgi limits, and letting Supervisor handle graceful restarts, you can eliminate fatal 500 crashes in under half an hour. The payoff is less churn, lower VPS bills, and a happier dev team.
Ready to supercharge your Laravel‑WordPress stack? Grab a low‑cost, high‑performance VPS from Hostinger and start deploying with confidence.
No comments:
Post a Comment