Risolvere l'errore nginx 504 Gateway Time-out: trovare la causa invece di alzare il termine

Pubblicato il 20 min di lettura

Con un 504 il backend era raggiungibile, ha solo risposto troppo lentamente. Come stabilire con il log dei tempi dove finisce il tempo, quale dei molti termini vale davvero e perché un termine più alto di solito sposta soltanto il disservizio.

Un 504 Gateway Time-out è la più paziente di tutte le pagine di errore. nginx ha accettato la richiesta, l'ha inoltrata al backend e poi ha aspettato fino allo scadere di un termine impostato internamente. Il backend è rimasto raggiungibile per tutto il tempo, semplicemente non ha risposto entro la scadenza. È esattamente qui che sta la differenza con il codice vicino: con il 502 Bad Gateway il backend risponde male oppure non risponde affatto, con il 504 risponde troppo lentamente. Questa guida ti mostra come misurare dove il tempo viene davvero consumato, quale dei molti termini entri realmente in gioco e perché alzare quel limite è quasi sempre la peggiore delle risposte a disposizione.

Tutte le indicazioni valgono per Debian 13 (trixie), Debian 12 (bookworm), Ubuntu 24.04 LTS e Ubuntu 22.04 LTS. I comandi sono scritti per l'esecuzione come root, se lavori come utente normale anteponi sudo. Negli esempi compare PHP 8.4, sostituisci il numero di versione con quello del tuo sistema:

SistemaPHPServizioConfigurazione
Debian 13 (trixie)8.4php8.4-fpm/etc/php/8.4/fpm/
Debian 12 (bookworm)8.2php8.2-fpm/etc/php/8.2/fpm/
Ubuntu 24.04 LTS8.3php8.3-fpm/etc/php/8.3/fpm/
Ubuntu 22.04 LTS8.1php8.1-fpm/etc/php/8.1/fpm/
ls /etc/php/

Le direttive di nginx portano lo stesso nome su tutti e quattro i sistemi. Le differenze stanno sul lato PHP e sul lato database, e sono segnalate nei punti in cui contano.

Chi ha mollato per primo decide da che parte cercare

Prima di aprire un file, rispondi a una domanda: quale livello ha interrotto? Il codice di stato lo dice già.

CodiceChe cosa è successoDove cercare
500 Internal Server ErrorIl backend ha risposto, la risposta era un erroreLog dell'applicazione
502 Bad GatewayLa connessione non si è stabilita oppure è cadutaServizio, socket, permessi, crash
504 Gateway Time-outLa connessione reggeva, la risposta non è arrivata entro il termineTempo di esecuzione nel backend
408 Request TimeoutIl visitatore non ha finito di inviare in tempo la propria richiestaUpload, connessioni lente
499 (solo nel log)Il visitatore ha interrotto prima che nginx finisseTroppo lento, ma sotto il termine

La riga con 499 è la più sottovalutata di tutte. Non è un errore, è un campanello d'allarme: il visitatore ha chiuso la scheda perché la pagina ci metteva troppo. Se un 504 sparisce dopo che hai alzato il termine e al suo posto compaiono dei 499, non hai risolto niente.

awk '{print $9}' /var/log/nginx/access.log | sort | uniq -c | sort -rn | head

Il campo 9 vale per il formato standard combined. Molti 504 a fronte di pochi 499 indicano singole pagine pesanti, il quadro opposto indica un'applicazione lenta in modo uniforme.

Prima di cambiare qualcosa: la via del ritorno

La diagnosi delle prossime due sezioni è di sola lettura. Il rischio comincia dove metti mano alle configurazioni, e lì il servizio può fermarsi in tre modi: una configurazione di nginx difettosa impedisce l'avvio del server web, un file del pool difettoso impedisce l'avvio di PHP-FPM, e limiti alzati con troppa generosità possono esaurire la RAM. L'ultimo caso è il più sgradevole, perché a quel punto il kernel termina dei processi e non colpisce per forza il responsabile. Se ci va di mezzo il servizio SSH, il server non è più governabile dalla rete.

Crea quindi prima di tutto delle copie, sotto /root e mai nella directory web. Il timestamp nel nome è importante, perché raramente si interviene una volta sola e cp -a sovrascrive senza dire niente una copia già presente:

mkdir -p /root/backups
cp -a /etc/nginx/nginx.conf /root/backups/nginx.conf.$(date +%F-%H%M)
cp -a /etc/nginx/sites-available/example.com /root/backups/example.com.$(date +%F-%H%M)
cp -a /etc/php/8.4/fpm/php.ini /root/backups/php.ini.$(date +%F-%H%M)
cp -a /etc/php/8.4/fpm/pool.d/www.conf /root/backups/www.conf.$(date +%F-%H%M)

La via del ritorno sono tre righe, e l'ordine non è casuale:

cp -a /root/backups/example.com.2026-09-03-1030 /etc/nginx/sites-available/example.com
nginx -t
systemctl reload nginx

Finché puoi, usa reload al posto di restart. reload applica la nuova configurazione soltanto se è priva di errori. Un restart invece termina per primo il processo in esecuzione e in caso di errore ti lascia senza server web.

Se il server non risponde più affatto, sui server root KVM e sui server dedicati di KernelHost apri la console VNC dall'area clienti. È agganciata al livello di virtualizzazione, o al collegamento stesso, e non allo stack di rete del sistema ospite: funziona quindi anche quando nessun servizio è più raggiungibile. Accedi una volta in anticipo e assicurati di conoscere la password di root. Comando di verifica dopo ogni intervento:

systemctl is-active nginx php8.4-fpm
free -m

La riga nel log degli errori che decide il caso

Un 504 lascia sempre una traccia:

2026/09/03 10:12:33 [error] 812#812: *5 upstream timed out (110: Connection timed out)
while reading response header from upstream, client: 203.0.113.7, server: example.com,
request: "GET /report.php HTTP/1.1", upstream: "fastcgi://unix:/run/php/php8.4-fpm.sock:"

A contare non è il numero di errore 110, identico in ogni 504, ma la fase che lo segue:

  • while connecting to upstream: la connessione non si è stabilita. Con un backend remoto è quasi sempre un filtro dei pacchetti che scarta i pacchetti invece di rifiutarli, perché un rifiuto tornerebbe indietro subito e produrrebbe un 502.
  • while sending request to upstream: nginx non è riuscito a consegnare il corpo della richiesta, tipico con gli upload di grandi dimensioni.
  • while reading response header from upstream: il caso normale. Il backend ha ricevuto tutto ed è al lavoro, senza spedire nemmeno la prima riga di intestazione.
  • while reading upstream: le intestazioni sono arrivate, poi il corpo si è inceppato. Lo vedi con le risposte in streaming e con le esportazioni.
grep -n "upstream timed out" /var/log/nginx/error.log | tail -20

Molti host virtuali scrivono in un log degli errori proprio. Dove si trova quel file e perché cercando in sites-enabled serve la -R maiuscola è spiegato nell'articolo sul 502. Se la ricerca resta vuota mentre il browser mostra un 504, l'errore non arriva da questo nginx.

Dove finisce il tempo: misurare invece di indovinare

nginx può registrare per ogni richiesta quanto tempo ha impiegato il backend. È il passo più importante della diagnosi, perché risponde senza congetture alla domanda "applicazione o connessione". Nel blocco http di /etc/nginx/nginx.conf:

log_format kh_timing '$time_iso8601 $status rt=$request_time '
                     'uct=$upstream_connect_time uht=$upstream_header_time '
                     'urt=$upstream_response_time "$request"';

Nel blocco server interessato aggiungi una seconda riga di log. Il log degli accessi esistente resta intatto, nginx scrive in entrambi:

access_log /var/log/nginx/timing.log kh_timing;
nginx -t
systemctl reload nginx
tail -n 5 /var/log/nginx/timing.log

Dopo qualche minuto di esercizio porta in cima le richieste più lente:

awk '{ t=$3; sub(/^rt=/, "", t); print t, $0 }' /var/log/nginx/timing.log | sort -rn | head -20
OsservazioneInterpretazionePasso successivo
uct alto con backend localeL'apertura della connessione si inceppaCoda del socket piena, risoluzione dei nomi lenta
uht e urt quasi uguali, entrambi altiIl backend elabora prima di inviare la prima intestazioneApplicazione, database, interfaccia esterna
uht basso, urt altoL'intestazione è arrivata subito, il corpo arriva a gocceStreaming, esportazioni, cicli su molti record
urt basso, rt altoIl backend è stato veloce, il tempo si è perso dopoConnessione del visitatore, risposta molto grande
Un trattino al posto di un numeroNon è stato coinvolto nessun backendFile statico oppure interruzione prima dell'inoltro

Due dettagli fanno risparmiare parecchio tempo. Più valori separati da virgola in un campo significano che la richiesta è andata a più di una destinazione, quindi c'è stato un secondo tentativo. E l'indizio più forte in assoluto: se la durata misurata corrisponde al secondo al valore configurato, per esempio 60,001 secondi con un termine di 60, allora è scattato il termine e non è stato il backend a mollare da solo. Valori irregolari come 43,7 secondi mostrano che a frenare è stato qualcos'altro.

Quale termine entra davvero in gioco

Per i timeout nginx conosce una buona dozzina di direttive, e l'ora sprecata più di frequente nasce dal fatto che qualcuno mette mano a quella sbagliata. Quale sia valida dipende da quale modulo elabora il blocco location.

DirettivaPredefinitoVale nei blocchi conEffetto allo scadere
proxy_connect_timeout60sproxy_pass504, "while connecting to upstream"
proxy_send_timeout60sproxy_pass504, "while sending request to upstream"
proxy_read_timeout60sproxy_pass504, il termine decisivo con i backend proxy
fastcgi_connect_timeout60sfastcgi_pass504, come sopra, per PHP-FPM
fastcgi_send_timeout60sfastcgi_pass504, come sopra
fastcgi_read_timeout60sfastcgi_pass504, il termine decisivo con PHP
send_timeout60sovunquenessun 504, viene chiusa la connessione verso il visitatore
client_body_timeout60sovunque408, non 504

Il termine vale tra due operazioni di lettura, non per l'intera risposta. Un download che dura dieci minuti e nel frattempo consegna dati senza interruzione arriva in fondo. Un backend che tace per 61 secondi viene buttato fuori. Con le esportazioni che si inceppano aiuta quindi più spesso far produrre qualcosa a intervalli regolari all'applicazione, invece di alzare il termine.

send_timeout non serve a niente contro un 504. Questo termine riguarda la trasmissione verso il visitatore. Se scade non ottieni una pagina di errore, ma un download interrotto. Diventa rilevante solo quando disattivi il buffering con proxy_buffering off;, perché a quel punto un visitatore lento frena fino dentro al backend.

nginx accetta anche direttive che nel blocco in questione non hanno alcun effetto. Un proxy_read_timeout 300s; dentro un blocco PHP con fastcgi_pass è sintatticamente ineccepibile, nginx -t risponde syntax is ok, e la pagina continua a interrompersi dopo 60 secondi. Al contrario vale lo stesso. È la causa più frequente per cui un termine alzato resta senza effetto. Che cosa valga davvero lo mostra la configurazione assemblata:

nginx -T | grep -E "read_timeout|send_timeout|fastcgi_pass|proxy_pass"

Se un singolo percorso deve davvero girare più a lungo, imposta il termine esattamente lì e da nessun'altra parte. Il segno di uguale ne fa una corrispondenza esatta, che vince sul blocco generico location ~ \.php$ incaricato degli altri file PHP:

location = /admin/export.php {
    include snippets/fastcgi-php.conf;
    fastcgi_pass unix:/run/php/php8.4-fpm.sock;
    fastcgi_read_timeout 300s;
}
nginx -t
systemctl reload nginx
curl -s -o /dev/null -w "%{http_code} %{time_total}\n" -H "Host: example.com" http://127.0.0.1/admin/export.php

PHP: perché qui max_execution_time interviene di rado

L'ipotesi più ovvia è che PHP chiuda da solo uno script bloccato. Proprio in questo caso quasi mai è così. Prima la trappola nella misurazione: php -i interroga la variante da riga di comando, che usa una configurazione propria sotto /etc/php/8.4/cli/ e gira comunque senza limite di esecuzione, quindi non dice niente su FPM. A contare sono questi due punti:

grep -n "^max_execution_time" /etc/php/8.4/fpm/php.ini
grep -rn "max_execution_time" /etc/php/8.4/fpm/pool.d/

Nel file del pool il valore può essere sovrascritto con php_value[max_execution_time] oppure php_admin_value[max_execution_time]. Un valore impostato con php_admin_value non è più modificabile dall'applicazione tramite ini_set(). Se il tuo framework alza da solo il tempo di esecuzione e all'improvviso questo non ha più effetto, la causa è proprio questa.

E ora il punto vero: su Linux il tempo di attesa nelle chiamate di sistema non viene conteggiato. L'orologio gira solo finché lo script sta effettivamente elaborando. Se aspetta una query al database, un'interfaccia esterna o il filesystem, resta fermo. Uno script può quindi restare incollato dieci minuti a una query bloccata senza che il limite di esecuzione scatti mai. A chiuderlo è allora il termine di nginx, e il risultato è il 504.

Ne segue una regola scomoda: max_execution_time protegge dai cicli infiniti nel tuo codice, non dall'attesa. L'unico limite duro sul lato PHP è request_terminate_timeout nel file del pool, che rimuove il processo di lavoro a prescindere da che cosa lo tenga bloccato. Produce però un 502 e non un 504. Ordina i termini in modo crescente dall'interno verso l'esterno, così interviene per primo il livello che può ancora produrre un messaggio comprensibile. Con gli script che elaborano funziona, con quelli che aspettano no, per il motivo appena detto. Lì resta solo una strada, limitare l'attesa stessa.

Perché alzare il termine è quasi sempre la risposta sbagliata

Fai due conti. Un pool con pm.max_children = 10 ha dieci processi di lavoro. Una pagina impiega 90 secondi. Dieci chiamate contemporanee occupano quindi ognuno di quei processi per un minuto e mezzo. In quel lasso di tempo nessuno riceve più una pagina PHP, nemmeno la pagina iniziale. Da una sottopagina lenta è nato un disservizio completo. Tre meccanismi lo amplificano:

  • Il visitatore ricarica. Questo genera una richiesta in più, ma non libera quella vecchia. PHP si accorge di un visitatore che ha interrotto solo quando lo script produce di nuovo qualcosa in output, e uno script che elabora non produce niente per parecchio tempo.
  • nginx ritenta da solo. In un blocco upstream con più destinazioni, proxy_next_upstream è impostato per impostazione predefinita su error timeout. Una richiesta scaduta passa al server successivo e la query costosa gira una seconda volta. Le richieste in scrittura sono escluse, quelle in lettura no. Si disattiva con proxy_next_upstream error;.
  • Anche il monitoraggio ritenta. Un intervallo di controllo di 60 secondi su una pagina che ne richiede 90 genera un carico permanente che non viene mai smaltito.

A questo si aggiunge che nessuno aspetta cinque minuti davanti a una pagina web. Un termine di 300 secondi trasforma un problema da un minuto in un problema da cinque minuti, e il visitatore se n'è andato da un pezzo mentre il processo di lavoro continua a elaborare.

Quanto sia davvero pieno lo mostra la pagina di stato di PHP-FPM. Imposta pm.status_path = /fpm-status nel file del pool e crea nel blocco server un accesso raggiungibile solo in locale:

location = /fpm-status {
    allow 127.0.0.1;
    deny all;
    include fastcgi_params;
    fastcgi_pass unix:/run/php/php8.4-fpm.sock;
}
systemctl reload php8.4-fpm
nginx -t
systemctl reload nginx
curl -s -H "Host: example.com" http://127.0.0.1/fpm-status

Guarda active processes, listen queue e max active processes. Se listen queue resta stabilmente sopra lo zero, il numero di processi di lavoro non basta per l'attuale tempo di esecuzione delle pagine. È il momento in cui devi abbassare quel tempo, non alzare il termine.

I tre soliti divoratori di tempo

Database

Nella maggior parte dei casi il tempo si nasconde qui. Guarda prima di tutto che cosa sta girando in questo momento. Su tutti e quattro i sistemi l'accesso root passa per impostazione predefinita dal socket Unix, quindi il comando funziona senza password:

mysql -e "SHOW FULL PROCESSLIST;"

Interessano le colonne Time e State. Valori come Sending data o Waiting for table metadata lock con secondi a due cifre sono il tuo caso. Per cercare in modo sistematico usa il log delle query lente, attivabile a servizio acceso:

mysql -e "SET GLOBAL slow_query_log = 1; SET GLOBAL long_query_time = 1;"
mysql -e "SHOW VARIABLES LIKE 'slow_query_log_file';"

Il nome del file lo leggi dal secondo output, perché cambia da sistema a sistema: Debian punta di solito su MariaDB e scrive in /var/log/mysql/mariadb-slow.log, su Ubuntu il file ha un altro nome a seconda del server installato. Dopo qualche minuto analizza e disattiva di nuovo, perché il log costa carico in scrittura:

mysqldumpslow -s t /var/log/mysql/mariadb-slow.log | head -30
mysql -e "SET GLOBAL slow_query_log = 0;"

L'opzione -s t ordina per tempo complessivo. La query più costosa la esamini con EXPLAIN, quasi sempre manca un indice proprio sulla colonna usata per filtrare o ordinare. SET GLOBAL ha effetto immediato, ma non sopravvive a un riavvio del database. Un limite massimo per singola query esiste anche sul lato database: MariaDB conosce max_statement_time in secondi, MySQL max_execution_time in millisecondi, quest'ultimo solo per le query in lettura. Così la tua applicazione riceve un errore pulito invece di tenere occupato un processo di lavoro.

Interfacce esterne

Se la tua pagina interroga a ogni chiamata un fornitore di servizi di pagamento o un server di licenze, il guasto di quel servizio diventa il tuo 504. Misura la chiamata separatamente, dal server:

curl -o /dev/null -s -w "dns=%{time_namelookup} connect=%{time_connect} ttfb=%{time_starttransfer} total=%{time_total}\n" https://api.example.com/status

Se è già dns a saltare all'occhio, la colpa è della risoluzione dei nomi e non della controparte, controprova con time getent hosts api.example.com. Nel codice ogni chiamata esterna ha bisogno di un termine proprio e stretto: con cURL sono CURLOPT_CONNECTTIMEOUT e CURLOPT_TIMEOUT. Chi invece usa file_get_contents() su un indirizzo finisce su default_socket_timeout del php.ini, dove per impostazione predefinita ci sono 60 secondi:

grep -n "default_socket_timeout" /etc/php/8.4/fpm/php.ini

La regola pratica: la somma di tutti i termini esterni di una richiesta deve restare sotto fastcgi_read_timeout. Altrimenti nginx taglia prima che il tuo codice possa produrre una pagina di errore comprensibile.

Filesystem e RAM

Un disco pieno rende le scritture lente o impossibili, e questo colpisce insieme file di sessione, cache e log. Verifica entrambe le cose, lo spazio e gli inode:

df -h
df -i

Il secondo comando è quello che si dimentica più spesso. Una partizione può essere occupata al 40% e non accogliere comunque nessun nuovo file, se gli inode sono esauriti. Il secondo candidato è la carenza di memoria: se il sistema comincia a usare lo swap, ogni richiesta diventa lenta senza che la colpa sia di una query precisa.

vmstat 1 5

Se le colonne si e so restano stabilmente sopra lo zero, il kernel scarica e ricarica memoria senza sosta e il tuo è un problema di memoria, non di tempo. Come affrontarlo in modo pulito è spiegato in configurare lo swap ed evitare l'Out-of-Memory. Il terzo candidato sono le unità di rete: un mount NFS bloccato blocca ogni processo che lo tocca, e questo vale anche per il tuo comando di diagnosi. Mettici quindi davanti un termine:

timeout 5 df -h

I compiti lunghi non stanno dentro la richiesta

Certi compiti richiedono tempo, punto: un rapporto annuale, un'importazione da 200.000 righe, una conversione di immagini. L'errore non sta nel fatto che durino a lungo, ma nel fatto che un server web li stia ad aspettare. La forma pulita ha tre parti:

  1. La richiesta crea un job, in una tabella o in una coda, e risponde subito. Il codice di stato adatto è 202, insieme a un indirizzo dal quale si può interrogare lo stato di avanzamento.
  2. Un processo di lavoro fuori da nginx preleva i job e li porta a termine. Non ha nessun termine sul collo, perché nessuno lo sta aspettando.
  3. L'interfaccia interroga lo stato di avanzamento. Quella interrogazione è sempre veloce, per quanto lungo sia il compito.

Il processo di lavoro fallo girare come servizio a sé, così torna da solo dopo un crash e dopo un riavvio. Come è fatta una unit del genere lo mostra creare un servizio systemd. Per i compiti a intervalli fissi basta un cronjob, e lì ti serve un lock perché due esecuzioni non si sovrappongano. Con -n la seconda esecuzione si interrompe subito invece di mettersi in attesa:

flock -n /run/lock/kh-worker.lock /usr/bin/php /var/www/html/worker.php

Per i framework più diffusi la coda esiste già pronta, va soltanto messa in esercizio: con Laravel php artisan queue:work, con Symfony php bin/console messenger:consume seguito dal nome del tuo transport. Entrambi vanno in una unit systemd, non in una finestra di terminale.

Un caso particolare merita una menzione esplicita: per impostazione predefinita WordPress avvia le attività pianificate dentro le richieste dei visitatori. Un visitatore paga quindi con la propria attesa il fatto che in background giri un controllo degli aggiornamenti. La riga define('DISABLE_WP_CRON', true); in wp-config.php, sopra il riferimento a wp-settings.php, disattiva questo comportamento. Da lì in poi le attività in scadenza le richiami tu stesso a intervalli regolari:

wp cron event run --due-now --path=/var/www/html

Quello che deve per forza restare sincrono riceve un blocco location proprio, con un termine proprio e in più un limite, così quell'unico percorso non occupa tutti i processi di lavoro: limit_conn_zone $binary_remote_addr zone=export:10m; nel blocco http e limit_conn export 1; nel blocco interessato. Gli ulteriori tentativi ricevono allora un 503 invece di trovare un server occupato.

Errori frequenti e soluzioni

Messaggio parola per parolaSignificato e rimedio
upstream timed out (110: Connection timed out) while reading response header from upstreamIl caso normale. Il backend impiega troppo tempo. Analizza il log dei tempi e individua il divoratore di tempo prima di mettere mano al termine.
upstream timed out (110: Connection timed out) while connecting to upstreamL'apertura della connessione è finita nel termine. Con un backend remoto è quasi sempre un filtro dei pacchetti che scarta invece di rifiutare, con PHP-FPM locale una coda del socket piena.
upstream timed out (110: Connection timed out) while reading upstreamLe intestazioni sono arrivate, poi il corpo si è fermato più a lungo del termine. Tipico delle esportazioni che a metà strada elaborano a lungo.
nginx: [emerg] "fastcgi_read_timeout" directive is not allowed here in /etc/nginx/nginx.conf:12La direttiva si trova fuori da http, server o location, di solito per sbaglio in cima al file.
nginx: [emerg] unknown directive "proxy_read_timout" in /etc/nginx/sites-enabled/example.com:31Errore di battitura. nginx controlla i nomi, non le intenzioni. Il numero di riga è scritto nel messaggio.
PHP Fatal error: Maximum execution time of 30 seconds exceeded in /var/www/html/export.php on line 42Qui il limite di esecuzione di PHP è intervenuto per una volta, quindi lo script stava elaborando e non aspettando. Produce un 500 o una pagina vuota, non un 504.
SQLSTATE[HY000]: General error: 1205 Lock wait timeout exceeded; try restarting transactionUn'altra transazione tiene la riga, il termine predefinito è di 50 secondi. La causa è quasi sempre una transazione rimasta aperta troppo a lungo.
cURL error 28: Operation timed out after 60000 millisecondsUn'interfaccia esterna non risponde. Imposta nel codice un termine tuo, più breve, così la tua applicazione mantiene il controllo.
504 nel browser, ma la ricerca di upstream timed out resta vuotaIl 504 non arriva da questo nginx, ma da un servizio a monte come un load balancer o un secondo proxy. Alcuni segnalano per questo un codice di stato proprio.
L'interruzione avviene ancora esattamente dopo 60 secondi, anche se il termine è impostato su 300La direttiva modificata appartiene al modulo sbagliato, oppure vince un altro blocco. nginx -T mostra che cosa vale davvero.

Come riconosci che il problema è risolto

Un comando senza messaggio di errore non dimostra niente, e nemmeno una singola chiamata andata a buon fine. Quattro prove che insieme reggono:

  1. Codice di stato e durata direttamente sul server, così nessuna cache abbellisce il risultato. Ci si aspetta un 200 e una durata ben al di sotto del termine. Un 200 dopo 58 secondi con un termine di 60 non è un successo, è il prossimo disservizio appena il carico sale un poco. Quello che conta è la peggiore di venti chiamate:
    for i in $(seq 1 20); do curl -s -o /dev/null -w "%{http_code} %{time_total}\n" -H "Host: example.com" http://127.0.0.1/report.php; done | sort -k2 -n | tail -3
  2. Il log degli errori resta muto. Prima del test azzeralo con truncate -s 0 /var/log/nginx/error.log, scatena le chiamate e guardaci di nuovo dentro. Un file vuoto è la prova vera.
  3. La coda di PHP-FPM è a zero. Finché nella pagina di stato sotto listen queue c'è qualcosa in attesa, la causa è soltanto spostata.
  4. Un riavvio non cambia niente. È il punto che si salta più spesso. I valori impostati con SET GLOBAL, i processi avviati a mano e le directory create da te sotto /run non gli sopravvivono. Verifica systemctl is-enabled nginx php8.4-fpm e riavvia il server una volta in modo controllato, finché sei ancora lì a guardare.

Questo riavvio, accesso alla console compreso, sui server root KVM e sui server dedicati di KernelHost lo esegui dall'area clienti, anche quando il servizio web in quel momento non consegna più niente. I server si trovano nel datacenter maincubes di Francoforte sul Meno (certificato TÜV TIER3+), con un filtraggio a monte nella rete che li precede.

Checklist rapida per l'emergenza

  1. Conta i codici di stato nel log degli accessi. 504, 499 e 502 messi uno accanto all'altro dicono più di ognuno di loro preso da solo.
  2. grep "upstream timed out" /var/log/nginx/error.log, annota la fase indicata nel messaggio.
  3. Attiva il log dei tempi e confronta uct, uht e urt. Se la durata coincide al secondo con il termine configurato, è scattato il termine.
  4. Individua il divoratore di tempo: database, interfaccia esterna, filesystem o memoria.
  5. Con nginx -T verifica quale termine vale in quel blocco, prima di modificarne uno.
  6. Solo dopo decidi: risolvere, spostare in una coda oppure, come ultima scelta, alzare il termine per quell'unico percorso.
  7. Dopo il fix: azzera il log, misura venti chiamate, riavvia il server una volta in modo controllato.

Domande frequenti

Qual è la differenza tra 502 Bad Gateway e 504 Gateway Time-out?
Con il 502 la connessione verso il backend non si stabilisce oppure cade, quindi il backend risponde male o non risponde affatto. Con il 504 la connessione reggeva, solo che la risposta non è arrivata entro il termine: il backend è rimasto raggiungibile per tutto il tempo e ha semplicemente risposto troppo lentamente. Un 500 significa invece che il backend ha risposto e che la risposta era un errore. Da qui la direzione della ricerca: con il 502 controlli servizio, socket e permessi, con il 504 il tempo di esecuzione nel backend.
Ho impostato fastcgi_read_timeout su 300 secondi, ma la pagina si interrompe comunque dopo 60 secondi. Da che cosa dipende?
Quasi sempre dal fatto che la direttiva modificata appartiene al modulo sbagliato oppure che vince un altro blocco. nginx accetta anche direttive che nel blocco in questione non hanno alcun effetto: un proxy_read_timeout 300s; dentro un blocco PHP con fastcgi_pass è sintatticamente ineccepibile, nginx -t risponde "syntax is ok", e la pagina continua a interrompersi dopo 60 secondi. Al contrario vale lo stesso. Che cosa valga davvero lo mostra la configurazione assemblata con nginx -T e una ricerca di read_timeout, send_timeout, fastcgi_pass e proxy_pass. Se un singolo percorso deve girare più a lungo, imposta il termine in un blocco location proprio con il segno di uguale, perché una corrispondenza esatta vince sul blocco PHP generico.
Un termine più alto risolve il problema?
Nella maggior parte dei casi lo sposta soltanto. Un pool con pm.max_children = 10 ha dieci processi di lavoro. Se una pagina impiega 90 secondi, dieci chiamate contemporanee occupano ognuno di quei processi per un minuto e mezzo, e in quel lasso di tempo nessuno riceve più una pagina PHP, nemmeno la pagina iniziale. Da una sottopagina lenta è nato così un disservizio completo. In più nessuno aspetta cinque minuti davanti a una pagina web. Se il 504 sparisce dopo che hai alzato il termine e al suo posto nel log degli accessi compaiono dei 499, a interrompere è stato il visitatore stesso e non hai risolto niente.
Quale riga del log degli errori appartiene a un 504?
Un 504 lascia sempre una riga della forma "upstream timed out (110: Connection timed out) while reading response header from upstream", che trovi con grep -n "upstream timed out" /var/log/nginx/error.log. A contare non è il numero di errore 110, identico in ogni 504, ma la fase che lo segue: "while connecting to upstream" indica una connessione che non si è mai stabilita, "while sending request to upstream" che nginx non è riuscito a consegnare il corpo della richiesta, "while reading response header from upstream" il caso normale, in cui il backend elabora senza spedire nemmeno la prima riga di intestazione, e "while reading upstream" che dopo le intestazioni il corpo si è inceppato.
Perché max_execution_time non chiude il mio script PHP bloccato?
Perché su Linux il tempo di attesa nelle chiamate di sistema non viene conteggiato. L'orologio gira solo finché lo script sta effettivamente elaborando. Se aspetta una query al database, un'interfaccia esterna o il filesystem resta fermo, e lo script può restare incollato dieci minuti a una query bloccata senza che il limite di esecuzione scatti mai. A chiuderlo è allora il termine di nginx, e il risultato è il 504. Fai attenzione anche alla trappola nella misurazione: php -i interroga la variante da riga di comando, che usa una configurazione propria e gira comunque senza limite di esecuzione. A contare sono il php.ini di FPM e il file del pool. L'unico limite duro sul lato PHP è request_terminate_timeout, che però produce un 502 e non un 504.
Come riconosco se è scattato il termine oppure se è stato il backend a mollare da solo?
Dalla durata misurata. Con un log_format tuo registra in un file aggiuntivo i valori $request_time, $upstream_connect_time, $upstream_header_time e $upstream_response_time. Se la durata corrisponde al secondo al valore configurato, per esempio 60,001 secondi con un termine di 60, allora è scattato il termine e non è stato il backend a mollare da solo. Valori irregolari come 43,7 secondi mostrano che a frenare è stato qualcos'altro. Più valori separati da virgola in un campo indicano un secondo tentativo verso una seconda destinazione, un trattino al posto di un numero significa che non è stato coinvolto nessun backend.
Il browser mostra 504, ma la ricerca di "upstream timed out" resta vuota. Da dove arriva l'errore?
Allora il 504 non arriva da questo nginx, ma da un servizio a monte come un load balancer o un secondo proxy, e alcuni segnalano per questo un codice di stato proprio. Prima però verifica ancora una cosa: molti host virtuali scrivono in un log degli errori proprio, quindi è possibile che tu stia cercando nel file sbagliato.
Che cosa faccio con i compiti che per natura durano più a lungo di qualsiasi termine ragionevole?
Portali fuori dalla richiesta. La richiesta crea un job e risponde subito, con il codice di stato 202 e un indirizzo dal quale si può interrogare lo stato di avanzamento. Un processo di lavoro fuori da nginx porta a termine il job senza nessun termine sul collo, e l'interfaccia si limita a interrogare lo stato. Fai girare quel processo di lavoro come servizio a sé, così torna da solo dopo un crash e dopo un riavvio, e proteggi le esecuzioni ricorrenti con flock -n perché due esecuzioni non si sovrappongano. Quello che deve per forza restare sincrono riceve un blocco location proprio, con un termine proprio e in più un limite tramite limit_conn, così quell'unico percorso non occupa tutti i processi di lavoro.

nginx PHP-FPM 504 Gateway Time-out Timeout Debian Ubuntu Risoluzione dei problemi Amministrazione Linux