Laravel Queue Workers Stuck on VPS: Why PHP‑FPM MySQL Deadlock Is Killing Your Daily Jobs and How to Fix It in Minutes
You’ve watched the queue dashboard freeze, your users complain about missing emails, and the CPU spikes on your VPS while the workers keep looping on the same 10 % of jobs. It feels like an endless loop of frustration—until you discover the hidden deadlock between PHP‑FPM and MySQL that is silently throttling every Laravel job you run.
Why This Matters
Queue workers are the backbone of any modern Laravel or WordPress SaaS. They process notifications, generate PDFs, sync data to third‑party APIs, and keep your site responsive. When those workers stall, revenue drops, support tickets rise, and the whole system looks unreliable. A single MySQL deadlock triggered by an aggressive PHP‑FPM pool can stall hundreds of jobs in seconds, turning a healthy VPS into a bottleneck nightmare.
Common Causes
- Over‑provisioned PHP‑FPM workers sharing the same DB connection pool.
- Long‑running transactions that lock rows for minutes.
- Missing
SELECT … FOR UPDATEindexes leading to deadlock cycles. - Supervisor
numprocsset higher than CPU cores, causing context‑switch thrash. - Redis cache miss causing fallback to DB for every job.
Step‑By‑Step Fix Tutorial
1. Diagnose the Deadlock
# Check MySQL InnoDB status
mysql -u root -p -e "SHOW ENGINE INNODB STATUS\G" | grep -i deadlock -A5
# Tail Laravel queue logs
tail -f storage/logs/laravel-queue.log | grep -i deadlock
2. Reduce PHP‑FPM Children
# /etc/php/8.2/fpm/pool.d/www.conf
pm = dynamic
pm.max_children = 12 # match (CPU cores * 2)
pm.start_servers = 4
pm.min_spare_servers = 2
pm.max_spare_servers = 6
php_admin_value[error_log] = /var/log/php-fpm.error.log
php_admin_flag[log_errors] = on
pm.max_children between 8‑12. More workers than cores cause excessive context switches and amplify deadlock windows.3. Optimize MySQL Transaction Scope
DB::transaction(function () {
$order = Order::where('id', $id)
->lockForUpdate()
->first();
// Do minimal work inside the lock
$order->status = 'processing';
$order->save();
});
4. Enable Redis Queue Backend
# .env
QUEUE_CONNECTION=redis
REDIS_HOST=127.0.0.1
REDIS_PASSWORD=null
REDIS_PORT=6379
# config/queue.php => 'redis' driver already configured
5. Restart Services in the Correct Order
sudo systemctl restart redis
sudo systemctl restart php8.2-fpm
sudo supervisorctl reread
sudo supervisorctl update
sudo supervisorctl restart laravel-worker:* # ensure fresh workers
VPS or Shared Hosting Optimization Tips
- Swap Size: Keep swap under 2 GB on VPS; too much swap causes latency.
- CPU Governor: Set
performancemode for burst workloads. - Apache vs Nginx: Prefer Nginx as a reverse proxy; it frees PHP‑FPM from handling static assets.
- Shared Hosting: If you cannot adjust PHP‑FPM, switch queue driver to
databaseand use a dedicated MySQL user with READ‑COMMITTED isolation.
Real World Production Example
Acme SaaS runs a Laravel API on a 2 vCPU Ubuntu 22.04 VPS with Nginx, PHP‑FPM 8.2, and Redis 6.0. After a traffic spike, the email:send queue stalled. The following changes recovered the system in under 10 minutes:
- Reduced
pm.max_childrenfrom 30 to 10. - Added a Redis cache layer for user settings, cutting DB reads by 70 %.
- Implemented
SELECT … FOR UPDATEon theemailstable. - Switched Nginx fastcgi buffers to
16kfor smoother PHP‑FPM responses.
Before vs After Results
| Metric | Before | After |
|---|---|---|
| Jobs/min | 120 | 480 |
| CPU avg % | 85% | 45% |
| DB lock wait | 12 s | 0.3 s |
Security Considerations
- Never run queue workers as
root. Use a dedicatedlaraveluser withsudo -urights only where needed. - Enable MySQL
sql_mode=STRICT_TRANS_TABLESto prevent silent data loss. - Lock down Redis with a strong password and bind to
127.0.0.1or your private network. - Use
php-fpmlisten.ownerandlisten.groupto restrict socket access.
innodb_lock_wait_timeout without understanding the impact can cause runaway transactions. Keep it at the default 50 seconds unless you have a proven use‑case.Bonus Performance Tips
- Run
composer install --optimize-autoloader --no-devon production. - Enable
opcache.validate_timestamps=0and setopcache.max_accelerated_files=10000. - Use
php artisan schedule:workonly when you need sub‑minute granularity; otherwise rely on cron. - Set Nginx
gzipandbrotlifor API responses. - Leverage Cloudflare page rules to cache static assets, reducing VPS load.
FAQ
Q: My queue works locally but stalls on the VPS. Why?
A: Local environments usually use SQLite or a single MySQL instance without contention. The VPS introduces concurrency, limited CPU, and shared RAM—all perfect conditions for deadlocks.
Q: Should I use
databaseorredisdriver?A:
redisis recommended for high‑throughput jobs because it removes the DB lock window entirely. Usedatabaseonly when Redis is unavailable.
Final Thoughts
Deadlocks between PHP‑FPM and MySQL are not a “code bug” – they are a systems‑level symptom. By aligning your VPS resources, trimming PHP‑FPM pools, tightening MySQL transactions, and offloading queue state to Redis, you can turn a stuck queue into a high‑velocity pipeline in minutes.
Remember: the best performance gains come from a holistic view. Adjust one layer, measure, then move to the next. Your next SaaS launch will thank you.
No comments:
Post a Comment