Laravel Queue Workers Stuck After Deployment on DigitalOcean VPS: Why My Threads Are Frozen & How I Fixed It in 15 Minutes
You’ve just pushed a fresh Laravel release to a DigitalOcean droplet, the API looks slick, but your background jobs? They’re — dead in the water. No logs, no errors, just a silent “queued” state that never moves. If you’ve ever stared at php artisan queue:work humming idle while your users wait, keep reading. This guide cuts through the noise, pinpoints the exact cause, and shows you a bullet‑proof 15‑minute fix.
Why This Matters
Queue workers are the heartbeat of any Laravel SaaS. Missed emails, delayed notifications, and frozen order processing are not just annoyances—they cost revenue and damage reputation. On a VPS you control the entire stack, which means you also control the failure points. Understanding why workers freeze after a deployment saves you from endless reload loops, warm‑up nightmares, and pricey support tickets.
Common Causes
- Supervisor losing its process ID after a zero‑downtime deploy.
- PHP‑FPM pool limits (pm.max_children, pm.max_requests) hitting a hard ceiling.
- Redis connection time‑out caused by
protected-mode yeson the new droplet. - Missing
.envvalues after a git pull (e.g.,QUEUE_CONNECTIONswitched tosync). - File‑system permission changes that block
storage/framework/queuesfor the www‑data user.
Step‑by‑Step Fix Tutorial
1. Verify the Queue Driver
# Check .env
cat .env | grep QUEUE_CONNECTION
# Expected output:
QUEUE_CONNECTION=redis
sync, change it to redis and run php artisan config:clear.2. Restart Supervisor with Proper Configuration
# /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 --daemon
autostart=true
autorestart=true
user=www-data
numprocs=4
redirect_stderr=true
stdout_logfile=/var/log/laravel/queue.log
After editing, run:
sudo supervisorctl reread
sudo supervisorctl update
sudo supervisorctl restart laravel-queue:*
numprocs based on your pm.max_children in PHP‑FPM (usually 4–8 per core).3. Tune PHP‑FPM Pool
# /etc/php/8.2/fpm/pool.d/www.conf
pm = dynamic
pm.max_children = 20
pm.start_servers = 4
pm.min_spare_servers = 2
pm.max_spare_servers = 8
pm.max_requests = 500
Then reload:
sudo systemctl restart php8.2-fpm
4. Harden Redis Connection
# /etc/redis/redis.conf
protected-mode no
bind 127.0.0.1
port 6379
timeout 0
Restart Redis and confirm connectivity:
sudo systemctl restart redis
redis-cli ping
# PONG
5. Check File Permissions
sudo chown -R www-data:www-data /var/www/html/storage
sudo chmod -R 775 /var/www/html/storage
6. Verify Worker Health
sudo supervisorctl status laravel-queue:*
# You should see RUNNING for all processes
VPS or Shared Hosting Optimization Tips
- Use a dedicated swap file (2 GB) on low‑memory droplets to prevent OOM kills.
- Enable OPCache in
php.ini– it reduces script compile time by up to 70 %. - Place Redis on a separate droplet or use DigitalOcean Managed Redis for production traffic.
- Activate Cloudflare “Cache‑Everything” for static assets, but create a page rule to bypass the API endpoints that hit the queue.
- Run Composer with
--no-dev --optimize-autoloaderduring CI/CD to keep the autoloader lean.
Real World Production Example
Acme SaaS runs on a 2 vCPU, 4 GB Ubuntu 22.04 droplet. After a minor code push, the queue stopped processing. The root cause was a missing QUEUE_CONNECTION entry in the newly merged .env.example file, causing the deploy script to overwrite the live .env. The quick fix above restored service in under 12 minutes with zero downtime for end‑users.
Before vs After Results
| Metric | Before Fix | After Fix |
|---|---|---|
| Jobs Processed/min | 0 | 150+ |
| Average Job Latency | >5 min | <2 sec |
| CPU Utilization | 90 % (idle workers) | 30 % (active) |
Security Considerations
When you open ports for Redis or adjust PHP‑FPM, never expose them to the public internet. Use DigitalOcean firewalls to restrict 6379 to the droplet’s private network only. Harden SSH with key‑based auth and disable root login. Lastly, keep Composer dependencies up‑to‑date: composer audit and composer update --with-all-dependencies weekly.
Bonus Performance Tips
- Batch queue jobs – use
dispatch(new Job)->delay(now()->addSeconds(10))for low‑priority tasks. - Leverage Horizon for a visual dashboard; it also auto‑scales workers based on queue depth.
- Enable MySQL query cache (or use MariaDB) for read‑heavy API endpoints.
- Compress Nginx responses with
gzip on;and setbrotli on;for modern browsers. - Set proper
Cache-Controlheaders on static assets to offload traffic from the VPS.
FAQ
Q: Do I need Supervisor on a shared host?
A: Most shared environments don’t allow long‑running processes. Use Laravel’s
php artisan schedule:runvia cron for small queues, but for production consider a VPS.
Q: My queue works locally but not on DigitalOcean – why?
A: Check the VPS firewall, Redis bind address, and that the
.envon the droplet matches local settings.
Final Thoughts
Queue worker paralysis feels like a nightmare, but in most cases it’s a mis‑aligned stack configuration rather than a Laravel bug. By confirming the queue driver, restarting Supervisor with the right user, and tuning PHP‑FPM and Redis, you can get your jobs moving again in under fifteen minutes—no heavy‑handed reboot required.
Keep a checklist in your CI/CD pipeline, automate supervisorctl reload post‑deploy, and lock down your VPS firewall. The result? Faster APIs, happier users, and a healthier bottom line.
No comments:
Post a Comment