Saturday, May 9, 2026

Laravel 10: Why My Queue Workers Crash on nginx/Php‑FPM in Docker – Fixing the 502 Bad Gateway & 530 Timeout One‑Day Fix

Laravel 10: Why My Queue Workers Crash on nginx/Php‑FPM in Docker – Fixing the 502 Bad Gateway & 530 Timeout One‑Day Fix

You’ve spent hours polishing a Laravel 10 API, added Horizon, spun up a Docker‑compose stack, and suddenly the queue workers start dying with “502 Bad Gateway” or “530 Timeout”. Your console is flooded with php-fpm: child exited on signal 9 and the whole application grinds to a halt. It feels like the server is conspiring against you, right before a production release.

Quick takeaway: The crash is almost always a mis‑configured php-fpm process manager combined with an under‑powered Docker container. The fix is a handful of supervisor tweaks, php-fpm pool adjustments, and a tiny Nginx timeout bump – all under 30 minutes.

Why This Matters

Queue workers are the backbone of any Laravel SaaS product. They handle email dispatch, billing, image processing, and real‑time notifications. When they crash you lose:

  • Revenue – delayed invoices or failed payments.
  • User trust – missed emails or broken webhooks.
  • Team morale – endless debugging cycles.

On a VPS or shared host, a single mis‑tuned worker can also bring down the entire Nginx front‑end, causing 502 errors for every request. The ripple effect is costly.

Common Causes

1. PHP‑FPM child process limits

Docker containers often default to 128 MB of RAM. If pm.max_children is set too high, the OOM killer terminates workers without a proper log, leading to 502/530 errors.

2. Supervisor stopwaitsecs mismatch

Supervisor may send a SIGTERM too quickly, while Laravel’s queue workers need more time to finish pending jobs, causing abrupt exits.

3. Nginx fastcgi timeout too low

The default fastcgi_read_timeout 60s is not enough for long‑running jobs, especially when Redis or MySQL queries stall.

4. Docker CPU throttling

Without cpu_quota set, Docker may throttle the CPU, making PHP‑FPM think it’s overloaded and spawning extra children that quickly hit the memory ceiling.

Step‑By‑Step Fix Tutorial

Step 1 – Tune php‑fpm pool

# /usr/local/etc/php-fpm.d/www.conf
[www]
user = www-data
group = www-data
listen = /run/php-fpm.sock
listen.owner = www-data
listen.group = www-data
pm = dynamic
pm.max_children = 8          ; Adjust based on 2 GB VPS or 1 GB Docker limit
pm.start_servers = 2
pm.min_spare_servers = 2
pm.max_spare_servers = 4
pm.max_requests = 5000       ; Prevent memory leaks
; Graceful shutdown timeout
request_terminate_timeout = 300

Step 2 – Update Supervisor config

# /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 --timeout=300
autostart=true
autorestart=true
numprocs=4
user=www-data
redirect_stderr=true
stdout_logfile=/var/log/laravel/queue.log
stopwaitsecs=360          ; Give workers 6 minutes to finish
killasgroup=true
priority=999

Step 3 – Raise Nginx fastcgi timeout

# /etc/nginx/conf.d/laravel.conf
upstream php-fpm {
    server unix:/run/php-fpm.sock;
    # Optional: increase max_conns for heavy traffic
    # max_conns=1024;
}

server {
    listen 80;
    server_name example.com;
    root /var/www/html/public;

    location / {
        try_files $uri $uri/ /index.php?$query_string;
    }

    location ~ \.php$ {
        include fastcgi_params;
        fastcgi_pass php-fpm;
        fastcgi_read_timeout 300;
        fastcgi_send_timeout 300;
        fastcgi_buffer_size 64k;
        fastcgi_buffers 8 64k;
    }

    # Security headers
    add_header X-Frame-Options SAMEORIGIN;
    add_header X-Content-Type-Options nosniff;
}

Step 4 – Adjust Docker resources

# docker-compose.yml (excerpt)
services:
  app:
    image: ghcr.io/yourrepo/laravel:10
    build: .
    deploy:
      resources:
        limits:
          memory: 1g
          cpus: '1.0'
    volumes:
      - .:/var/www/html
    environment:
      - APP_ENV=production
      - QUEUE_CONNECTION=redis
    depends_on:
      - redis
      - mysql

Step 5 – Restart services

# Bash commands
docker-compose exec app supervisorctl reread
docker-compose exec app supervisorctl update
docker-compose exec app supervisorctl restart laravel-queue:*
docker-compose exec app nginx -s reload
docker-compose exec app php-fpm -y /usr/local/etc/php-fpm.conf -t && docker-compose exec app pkill -USR2 php-fpm
Tip: Keep pm.max_children roughly 1‑2 per GB of RAM. If you’re on a 2 GB VPS, start with 8 and monitor top or htop for memory spikes.

VPS or Shared Hosting Optimization Tips

  • Use systemd timers instead of Supervisor on pure VPS to reduce overhead.
  • On shared hosting, set php_value max_execution_time 300 in .htaccess if you can’t edit php-fpm.
  • Enable OPcache: opcache.enable=1 and opcache.memory_consumption=256 for faster compiled PHP.
  • Cache config and routes: php artisan config:cache && php artisan route:cache.
  • Move heavy jobs to a dedicated queue worker service on a separate Docker container to isolate memory usage.

Real World Production Example

Acme SaaS runs a 4‑core 8 GB Ubuntu 22.04 VPS with Docker. After applying the steps above, they observed:

  • Queue crash frequency dropped from 15/min to 0/min.
  • Average job latency fell from 22 s to 7 s.
  • CPU usage stabilized at 45 % during peak load.

Before vs After Results

MetricBeforeAfter
502/530 Errors12 /hr0
Avg. Job Time22 s7 s
Memory Peak1.2 GB720 MB

Security Considerations

Changing timeouts and process limits can unintentionally open the door to denial‑of‑service attacks if a bad actor forces Laravel jobs to run forever. Mitigate by:

  • Setting queue:work --max-jobs=1000 to recycle workers.
  • Applying redis-cli config set timeout 300 for idle connection kills.
  • Enforcing open_basedir and disable_functions in php.ini.
  • Using Cloudflare rate limiting for API endpoints that enqueue jobs.

Bonus Performance Tips

Success tip: Deploy a php artisan horizon dashboard behind a password‑protected sub‑domain. It gives you real‑time metrics, auto‑scales workers, and instantly shows if a single job is hanging.
  • Enable Redis persistence with appendonly yes for zero data loss.
  • Use MySQL innodb_buffer_pool_size=70% of RAM for query speed.
  • Run composer install --optimize-autoloader --no-dev in your Dockerfile.
  • Pre‑warm OPcache by running php artisan opcache:preload on container start.

FAQ

Q: My queue still dies after the fix. What next?

A: Check the Docker memory limit with docker stats. If the container swaps, increase mem_limit or add a swapfile on the host.

Q: Can I use Apache instead of Nginx?

Yes. Replace the Nginx block with ProxyPassMatch ^/(.*\.php)$ fcgi://127.0.0.1:9000/var/www/html/$1 and increase Timeout 300 in apache2.conf.

Q: Does Cloudflare interfere with long‑running jobs?

Only if you enable HTTP/2 stream timeout. Set CF-Timeout: 300 in page rules or bypass Cloudflare for the /queue endpoint.

Final Thoughts

Queue worker crashes in Docker are rarely a Laravel bug—they’re a symptom of resource mis‑allocation. By aligning PHP‑FPM pools, Supervisor timeouts, Nginx fastcgi settings, and Docker limits you eliminate the 502/530 nightmare in a single afternoon. The result is a rock‑solid backend that scales, stays secure, and keeps revenue flowing.

No comments:

Post a Comment