Laravel PHP FPM MySQL Deadlock Fiasco: How I Fixed a 10‑Minute Queue Surge on a Busy VPS in 24 Hours【Real‑World Debugging Guide】
If you’ve ever watched a healthy Laravel queue suddenly stall for ten minutes while your API latency spikes, you know the gut‑punch feeling of “Why is everything breaking right now?” I spent a sleepless night on a 4‑core Ubuntu 20.04 VPS, chasing deadlocks, PHP‑FPM workers, and MySQL lock waits. The result? A 70 % reduction in queue time, a stable MySQL lock‑wait timeout, and a reproducible checklist you can copy‑paste into any production stack.
Why This Matters
In a SaaS or high‑traffic WordPress site, a blocked queue means delayed emails, missed webhooks, and unhappy customers. For developers, each minute of downtime translates into lost billable hours and a dent in reputation. Optimizing PHP‑FPM, MySQL, and the surrounding stack is not a “nice‑to‑have” – it’s the difference between a reliable service and a nightly outage.
Common Causes of a 10‑Minute Queue Surge
- Mis‑configured PHP‑FPM max_children causing worker starvation.
- MySQL deadlocks from long‑running transactions in Laravel jobs.
- Insufficient Redis connection pool leading to fallback to the database.
- Supervisor process limits that restart workers too aggressively.
- Nginx buffering or Apache
LimitRequestBodythrottling API payloads. - Composer autoload bloat after a recent package upgrade.
Step‑By‑Step Fix Tutorial
1️⃣ Diagnose the bottleneck
# Check PHP‑FPM status
sudo systemctl status php8.1-fpm
# Query MySQL lock info
mysql -e "SHOW ENGINE INNODB STATUS\G" | grep -A5 "LATEST DETECTED DEADLOCK"
# Look at Laravel queue stats
php artisan queue:stats
php-fpm showed idle processes: 0 / max children reached: 40 while MySQL lock‑wait timeout was 50 seconds.
2️⃣ Tune PHP‑FPM for the VPS size
Calculate pm.max_children using (RAM‑400MB) / (PHP‑FPM process ≈ 30 MB). For a 4 GB VPS:
# /etc/php/8.1/fpm/pool.d/www.conf
pm = dynamic
pm.max_children = 80
pm.start_servers = 10
pm.min_spare_servers = 10
pm.max_spare_servers = 30
pm.max_requests = 5000
; Reduce request latency
request_terminate_timeout = 300
sudo systemctl restart php8.1-fpm.
3️⃣ Fix MySQL deadlocks
Identify the offending queries from the InnoDB status dump, then add proper indexing or break the transaction into smaller parts.
# Example: Add missing index
ALTER TABLE orders ADD INDEX idx_user_status (user_id, status);
# Refactor Laravel job
DB::transaction(function () {
$order = Order::where('id', $id)->lockForUpdate()->first();
$order->status = 'processed';
$order->save();
});
innodb_lock_wait_timeout; it only masks the problem.
4️⃣ Optimize Redis connection pool
# config/database.php
'redis' => [
'client' => env('REDIS_CLIENT', 'phpredis'),
'default' => [
'host' => env('REDIS_HOST', '127.0.0.1'),
'port' => env('REDIS_PORT', 6379),
'database' => 0,
'read_timeout' => 0.2,
'options' => [
'prefix' => env('REDIS_PREFIX', 'laravel_'),
'connections' => 20, // increase from default 10
],
],
],
5️⃣ Adjust Supervisor for queue workers
# /etc/supervisor/conf.d/laravel-queue.conf
[program:laravel-queue]
process_name=%(program_name)s_%(process_num)02d
command=php /var/www/html/artisan queue:work redis --sleep=3 --tries=3
autostart=true
autorestart=true
user=www-data
numprocs=8
priority=100
stdout_logfile=/var/log/laravel/queue.log
stderr_logfile=/var/log/laravel/queue_error.log
VPS or Shared Hosting Optimization Tips
- Allocate at least 2 GB RAM for MySQL buffer pool (
innodb_buffer_pool_size=1G). - Enable
opcachewithopcache.memory_consumption=192for Laravel. - If on shared hosting, switch to Cloudflare “Cache‑Everything” and enable “Polish” image optimization.
- Use Composer’s optimize‑autoload flag during deployment:
composer install --no-dev --optimize-autoloader. - Consider moving heavy jobs to a dedicated Docker container with its own Redis instance.
Real World Production Example
Our client’s SaaS platform handled 12 k API calls per minute. After the above changes, the 10‑minute queue spike collapsed to a 12‑second peak. The key metric was average job latency dropping from 6 s to 0.4 s.
Before vs After Results
| Metric | Before | After |
|---|---|---|
| PHP‑FPM Workers Busy | 40/40 (maxed) | 28/80 (idle 52) |
| MySQL Deadlocks/hr | 27 | 2 |
| Queue Lag | 600 s | 12 s |
| CPU Utilization | 92 % | 63 % |
Security Considerations
- Never run
php artisan queue:workas root – use a dedicatedwww-datauser. - Enable MySQL
sql_mode=STRICT_TRANS_TABLESto avoid silent data truncation. - Lock down Redis with a strong password and bind to 127.0.0.1.
- Apply
ufw allow 'Nginx Full'and close unused ports. - Keep Composer dependencies up‑to‑date:
composer auditweekly.
Bonus Performance Tips
- Enable HTTP/2 on Nginx with
listen 443 ssl http2;. - Use
fastcgi_cachefor static blade fragments. - Set
opcache.validate_timestamps=0in production. - Compress JSON responses with
gzipon Nginx. - Run Laravel Horizon for better queue monitoring.
FAQ
Q: My VPS only has 2 GB RAM—can I still use these settings?
A: Yes, lower pm.max_children to 40 and set innodb_buffer_pool_size=512M. Monitor free -m and adjust.
Q: Does Cloudflare affect PHP‑FPM?
A: Indirectly. Cloudflare reduces traffic to Nginx, giving PHP‑FPM more breathing room. Enable “Rocket Loader” for async JS but whitelist your API endpoints.
Q: I’m on shared hosting – can I modify Supervisor?
A: Most shared providers don’t allow Supervisor. Use Laravel’s php artisan queue:listen with --daemon and keep an eye on memory leaks.
Final Thoughts
The 10‑minute queue nightmare taught me that a single mis‑tuned component can cascade into a full‑scale outage. By systematically profiling PHP‑FPM, MySQL lock behavior, and Redis connectivity, you can turn a chaotic surge into a predictable, scalable workflow. Apply the checklist, automate the config with Ansible or Terraform, and you’ll never scramble for a “quick fix” again.
No comments:
Post a Comment