Match Your Timeout Chain: Nginx → PHP-FPM → PHP

📖 2 minutes read

You’ve bumped max_execution_time to 300 seconds in PHP, but your app still throws 504 Gateway Timeout after about a minute. Sound familiar?

The problem isn’t PHP. It’s the timeout chain — every layer between the browser and your code has its own timeout, and they all need to agree.

The typical Docker stack

Browser → Nginx (reverse proxy) → PHP-FPM → PHP script

Each layer has a default timeout:

Layer Setting Default
Nginx fastcgi_read_timeout 60s
PHP-FPM request_terminate_timeout 0 (unlimited)
PHP max_execution_time 30s

If you only change max_execution_time to 300s, PHP is happy to run that long — but Nginx kills the connection at 60 seconds because nobody told it to wait longer. You get a 504, PHP keeps running in the background (wasting resources), and your logs show no PHP errors because PHP didn’t fail.

The fix: align the entire chain

php.ini overrides:

max_execution_time = 300
max_input_time = 300
memory_limit = 512M

Nginx site config (inside your location ~ \.php$ block):

location ~ \.php$ {
    fastcgi_pass php:9000;
    include fastcgi_params;
    fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;

    # Match PHP's max_execution_time
    fastcgi_read_timeout 300s;
    fastcgi_send_timeout 300s;
    fastcgi_connect_timeout 60s;

    # Prevent 504 on large responses
    fastcgi_buffers 8 16k;
    fastcgi_buffer_size 32k;
    fastcgi_max_temp_file_size 0;
}

If you have a CDN or load balancer in front (Cloudflare, AWS ALB), add that to the chain too:

CDN (100s) → Nginx (300s) → PHP-FPM (0) → PHP (300s)
                                              ↑ This is the actual limit

The debugging trick

When you get a 504, test from the inside out:

  1. Hit PHP-FPM directly (bypass Nginx): Does the request complete?
  2. Hit Nginx locally (bypass CDN): Does it time out?
  3. Hit the public URL: Does the CDN add its own timeout?

This tells you exactly which layer is killing the request. Compare your local dev config against production — often the mismatch is the missing fastcgi_read_timeout that production has but your Docker setup doesn’t.

Rule of thumb: every layer’s timeout should be ≥ the layer below it. If PHP allows 300s, Nginx must allow at least 300s. If Nginx allows 300s, the CDN must allow at least 300s. One weak link and the whole chain breaks.

Daryle De Silva

VP of Technology

11+ years building and scaling web applications. Writing about what I learn in the trenches.

Comments

Leave a Reply

Your email address will not be published. Required fields are marked *