Quando uma página demora demais e volta 504 Gateway Timeout, não existe "o timeout do PHP": existem três relógios independentes correndo ao mesmo tempo, cada um pertencendo a uma camada diferente — max_execution_time (PHP), request_terminate_timeout (PHP-FPM) e fastcgi_read_timeout (nginx). Vence o menor. É exatamente por isso que aumentar um só não resolve: você levanta o teto de um relógio e continua batendo no outro, que ficou para trás. A boa notícia é que cada um deixa uma mensagem diferente — dá para saber em segundos qual disparou, em vez de mexer no escuro.
Quem é dono de cada relógio
Para entender por que são três, siga o caminho de uma requisição. O nginx recebe o pedido do navegador e o repassa ao PHP-FPM; ele então fica esperando a resposta — e tem um limite de paciência (fastcgi_read_timeout). O master do FPM entrega o pedido a um worker e fica vigiando quanto tempo aquele worker leva (request_terminate_timeout). Dentro do worker, o próprio PHP cronometra a execução do seu script (max_execution_time).
São três vigias observando a mesma requisição de pontos diferentes. Como cada um age sozinho ao estourar, o primeiro a chegar no limite define o que você vê:
| Relógio | Onde se configura | Padrão típico | O que faz ao estourar |
|---|---|---|---|
max_execution_time |
php.ini / pool |
30 s | Erro fatal no script; o PHP devolve a resposta |
request_terminate_timeout |
pool do FPM | 0 (desligado) |
Mata o worker e registra no log do FPM |
fastcgi_read_timeout |
nginx | 60 s | Desiste de esperar e devolve 504 ao navegador |
Repare numa assimetria importante: só o do nginx produz o 504 na tela do usuário. Os outros dois cortam do lado do PHP — e o que o visitante vê depende de qual foi.
Identificando o culpado pela mensagem
Cada relógio escreve num lugar diferente, com um texto característico. Este é o atalho do diagnóstico:
Se foi o PHP (max_execution_time)
O script morre com erro fatal e a mensagem vai para o log de erros do PHP. O usuário costuma receber uma página de erro (ou branca), não um 504.
PHP Fatal error: Maximum execution time of 30 seconds exceeded
in /var/www/relatorio.php on line 42
Se foi o FPM (request_terminate_timeout)
O master mata o worker e registra no log do FPM — com o script e o tempo real, o que é ótimo para diagnóstico.
WARNING: [pool www] child 2841, script '/var/www/relatorio.php'
(request: "GET /relatorio.php") execution timed out (35.002471 sec), terminating
Se foi o nginx (fastcgi_read_timeout)
O nginx desiste de esperar e devolve 504. A marca registrada está no error.log do nginx.
upstream timed out (110: Connection timed out)
while reading response header from upstream
O detalhe que fecha o diagnóstico
Se você vê o 504 no navegador e o log do nginx tem upstream timed out, mas o log do PHP está vazio, a conclusão é direta: o nginx cortou primeiro. O PHP nem chegou perto do limite dele — pode até ter continuado rodando depois.
504 + log do PHP vazio → o relógio do nginx é o menor
A ordem certa: PHP < FPM < nginx
A recomendação é escalonar os três valores de dentro para fora, com folga entre eles:
; PHP (php.ini do FPM, ou php_admin_value no pool)
max_execution_time = 30
; pool do FPM (ex.: /etc/php/8.3/pool.d/www.conf)
request_terminate_timeout = 35
; nginx (no location do PHP)
fastcgi_read_timeout = 60;
O porquê é prático: você quer que o relógio mais interno dispare primeiro. Se o PHP corta antes, você ganha uma mensagem de erro com arquivo e linha — o diagnóstico ideal. Se quem corta é o FPM, você ao menos sabe qual script travou. Se quem corta é o nginx, você fica com um 504 anônimo e um worker possivelmente ainda ocupado. O request_terminate_timeout funciona como uma rede de segurança logo acima do PHP: ele pega os casos em que o relógio do PHP não conseguiu agir — e a próxima seção mostra que esses casos são comuns.
; "está dando 504? aumenta o do PHP"
max_execution_time = 300
; ...e os outros dois ficaram como estavam:
; request_terminate_timeout = 0 (desligado)
; fastcgi_read_timeout = 60s ← o nginx corta em 60s do mesmo jeito
; Resultado: continua 504, e agora o worker fica preso 300s.; escalonado de dentro para fora, com folga
max_execution_time = 30 ; PHP — erro com arquivo e linha
request_terminate_timeout = 35 ; FPM — rede de segurança
fastcgi_read_timeout = 60; ; nginx — o mais tolerante
; e o principal: investigar POR QUE leva 30s (slowlog)O relógio do PHP tem um ponto cego
Esta é a parte que explica a maioria dos casos "aumentei tudo e continua". O max_execution_time não conta o tempo gasto fora do script — chamadas de sistema, esperas de rede, consultas ao banco. Ele mede o tempo de execução do PHP em si. Dá para provar em duas linhas:
// com max_execution_time = 1
sleep(3);
echo "sobreviveu"; // imprime! o sleep não conta para o relógio do PHP
Um laço de CPU nas mesmas condições morre na hora com Maximum execution time of 1 second exceeded. O sleep(3), não. Troque o sleep pelo que acontece no mundo real — uma query que demora 90 segundos, um curl para uma API que não responde, um file_get_contents remoto — e o efeito é o mesmo: o script fica parado esperando, e o relógio do PHP praticamente não anda.
Consequências diretas:
- Um script travado numa query lenta não é interrompido pelo
max_execution_time. Quem o mata é orequest_terminate_timeoutdo FPM (que mede tempo de parede) ou o nginx. - É por isso que deixar
request_terminate_timeout = 0(o padrão) é arriscado: sem essa rede, um banco lento pode prender workers indefinidamente — e workers presos levam aopm.max_childrenestourado e ao site inteiro na fila. - Sempre defina timeout nas chamadas externas (
CURLOPT_TIMEOUT, timeout do driver do banco). É a única proteção que age no lugar certo: dentro do seu código, com a chance de tratar o erro.
Você tem max_execution_time = 300 e uma página que consulta um relatório pesado no banco. Ela devolve 504 aos 60 segundos e o log do PHP está vazio. Qual é a explicação?
Ver resposta
Resposta certa: O fastcgi_read_timeout do nginx (60s) é o menor dos três e cortou primeiro
O 504 aos 60 segundos entrega o culpado: 60s é o padrão do fastcgi_read_timeout. O nginx desistiu de esperar e respondeu ao navegador — por isso o log do PHP está limpo, já que o script nem foi interrompido (provavelmente continuou rodando). Aumentar o max_execution_time para 300 não teve efeito nenhum, e nem teria: além de o nginx ser o menor relógio, o tempo de espera pelo banco quase não conta para o relógio do PHP. Confirme no error.log do nginx procurando por upstream timed out.
Aumentar timeout raramente é a solução
Ajustar os três valores conserta o sintoma e é necessário para não cortar no lugar errado — mas uma página que leva mais de 30 segundos tem um problema de arquitetura, não de configuração. Enquanto ela roda, um worker do FPM fica inteiramente ocupado (o PHP-FPM não é assíncrono): 10 requisições dessas em paralelo consomem 10 workers e podem derrubar o site inteiro por falta de workers livres.
O caminho certo, em ordem de esforço:
- Descubra onde o tempo vai. Ligue o slowlog do FPM (
request_slowlog_timeout+slowlog): ele grava o backtrace do script travado, com função e linha. - Ataque a causa. Quase sempre é índice faltando, N+1 de consultas, ou chamada externa sem timeout.
- Tire o trabalho pesado do request. Relatórios, e-mails em massa, processamento de arquivo e integrações lentas devem rodar numa fila ou num comando de CLI (onde o
max_execution_timeé0por padrão, e ninguém está esperando na frente do navegador). O usuário recebe "estamos processando" na hora e é avisado quando terminar.
Resumo de decisão
| Sintoma | Culpado | O que fazer |
|---|---|---|
504 + upstream timed out no log do nginx |
fastcgi_read_timeout |
É o menor relógio — alinhe os três |
execution timed out (N sec), terminating no log do FPM |
request_terminate_timeout |
Rede de segurança funcionou; investigue o script citado |
Maximum execution time of N seconds exceeded |
max_execution_time |
Melhor cenário: você tem arquivo e linha |
| 504 e nenhum log do PHP | nginx cortou primeiro | Não procure no log da aplicação |
| Script preso em query/API e nada o mata | Ponto cego do relógio do PHP | Defina request_terminate_timeout e timeout nas chamadas externas |
| A página legitimamente leva minutos | Arquitetura | Fila ou CLI, não timeout maior |
Em uma frase: descubra qual relógio disparou pela mensagem, escalone os três de dentro para fora — e depois trate o motivo de a página estar lenta, porque timeout maior só adia o problema segurando um worker por mais tempo.
A página de configuração do FPM documenta as diretivas de pool — request_terminate_timeout, request_slowlog_timeout, slowlog e companhia — com o significado exato de cada uma e as restrições entre elas.
Entender onde cada diretiva pode ser definida (php.ini, pool, .htaccess, ini_set em runtime) evita o clássico "mudei o valor e não pegou" — max_execution_time é um caso típico.