WEB Server

Nginx 504 Gateway Timeout

L’errore 504 Gateway Timeout è uno degli errori HTTP più comuni in ambienti che utilizzano nginx come reverse proxy. A differenza dell’errore 502 Bad Gateway, che indica una risposta non valida o assente dal backend, l’errore 504 ha una causa più specifica: il backend ha impiegato troppo tempo a rispondere e nginx ha abbandonato l’attesa prima di ricevere una risposta.

Nginx, agendo come gateway o proxy, non riceve una risposta dall’upstream server entro il tempo configurato per l’attesa. A quel punto, per non tenere occupate le risorse indefinitamente, interrompe l’attesa e restituisce un errore 504 al client.

Possiamo schematizzare il flusso in questo modo

Come per l’errore 502, il punto di partenza per individuare il problema è sempre la lettura dei log di nginx

grep " 504 " /var/log/nginx/access.log | tail -20

Questo comando mostra quali URL generano l’errore 504 e con quale frequenza. Se l’errore compare solo su determinate pagine (ad esempio quelle che eseguono import, report o elaborazioni pesanti), la causa è quasi certamente uno script lento.

Per identificare le richieste più lente, è possibile abilitare il logging dei tempi di risposta dell’upstream modificando il file di configurazione (/etc/nginx/nginx.conf)

http {
    log_format upstream_time '$remote_addr - $remote_user [$time_local] '
                             '"$request" $status $body_bytes_sent '
                             'rt=$request_time uct="$upstream_connect_time" '
                             'uht="$upstream_header_time" urt="$upstream_response_time"';

    access_log /var/log/nginx/access.log upstream_time;
}

Dopo il reload di nginx, ogni riga del log includerà il tempo di risposta dell’upstream (urt). Ora, per estrarre le richieste più lente possiamo usare questo comando

awk '{print $NF, $7}' /var/log/nginx/access.log | sort -rn | head -20

Come visto in precedenza per l’errore 502, nel file error.log di nginx possiamo trovare informazioni sulla causa

grep -E "timed out|upstream" /var/log/nginx/error.log | tail -20

# Esempio di riga di log
upstream timed out (110: Connection timed out) while reading response header from upstream,
client: 192.168.1.100, server: dominio.it,
request: "GET /report.php HTTP/1.1",
upstream: "fastcgi://unix:/run/php/php8.2-fpm.sock",
host: "dominio.it"

La parte “while reading response header from upstream” indica che nginx ha stabilito la connessione con PHP-FPM, ma quest’ultimo non ha inviato la risposta entro il timeout configurato.

I timeout di Nginx

Per il modulo ngx_http_proxy_module (proxy verso backend HTTP) esistono tre direttive di timeout. Nel 99% dei casi di errore 504, proxy_read_timeout è il responsabile: il backend è lento a rispondere, non lento ad accettare la connessione.

Per le applicazioni PHP con FastCGI, le direttive equivalenti sono

DirettivaDescrizioneDefault
fastcgi_connect_timeoutTempo massimo per stabilire la connessione con PHP-FPM60s
fastcgi_send_timeoutTempo massimo per inviare la richiesta a PHP-FPM60s
fastcgi_read_timeoutTempo massimo per ricevere la risposta da PHP-FPM60s

Per i backend HTTP (Node.js, altro Nginx, Apache)

DirettivaDescrizioneDefault
proxy_connect_timeoutTempo per stabilire la connessione60s
proxy_send_timeoutTempo per inviare la richiesta60s
proxy_read_timeoutTempo per ricevere la risposta60s

Timeout FastCGI troppo breve

È la causa più frequente. Lo script PHP esegue un’operazione che supera il timeout predefinito di 60 secondi. Ad esempio un import di dati, un report su larga scala, una chiamata a un’API esterna e nginx abbandona l’attesa prima che PHP-FPM abbia terminato.

# Messaggio nel log
upstream timed out (110: Connection timed out) while reading response header from upstream

# Aumentare il timeout in Nginx - /etc/nginx/sites-enabled/il-tuo-sito
location ~ \.php$ {
    fastcgi_pass            unix:/run/php/php8.2-fpm.sock;
    fastcgi_read_timeout    120;
    fastcgi_send_timeout    120;
    fastcgi_connect_timeout 120;
    fastcgi_param           SCRIPT_FILENAME $document_root$fastcgi_script_name;
    include                 fastcgi_params;
}

# Allineare il timeout in PHP-FPM - /etc/php/8.2/fpm/pool.d/www.conf
request_terminate_timeout = 120

Regola fondamentale: ogni layer esterno deve avere un timeout maggiore o uguale al layer interno. Il timeout di nginx (fastcgi_read_timeout) deve essere maggiore o uguale al valore request_terminate_timeout di PHP-FPM. Se PHP-FPM termina lo script dopo 120 secondi ma nginx abbandona dopo 60, si otterrà sempre un errore 504 perché PHP-FPM sta ancora elaborando.

Una buona prassi è aumentare anche il valore max_execution_time nella configurazione di PHP (/etc/php/8.2/fpm/php.ini)

max_execution_time = 120

per rendere attive le modifiche

systemctl restart php8.2-fpm && nginx -t && systemctl reload nginx

Script PHP lento

Aumentare i timeout è una soluzione valida per operazioni legittime e prevedibili (import, report). Tuttavia, se le richieste ordinarie impiegano decine di secondi, il problema è nell’applicazione, non nella configurazione.

Le cause più comuni di script PHP lenti possono essere:

Query SQL non ottimizzate: una SELECT senza indice su una tabella con milioni di righe può richiedere molto tempo. Possiamo verificare se siamo in questa condizione eseguendo la seguente query su MySQL

EXPLAIN SELECT * FROM tabella WHERE campo = 'valore';

+----+-------------+--------+------------+------+---------------+------+---------+------+---------+----------+-------------+
| id | select_type | table  | partitions | type | possible_keys | key  | key_len | ref  | rows    | filtered | Extra       |
+----+-------------+--------+------------+------+---------------+------+---------+------+---------+----------+-------------+
|  1 | SIMPLE      | ordini | NULL       | ALL  | NULL          | NULL | NULL    | NULL | 2847392 |    10.00 | Using where |
+----+-------------+--------+------------+------+---------------+------+---------+------+---------+----------+-------------+

Se il valore della colonna type è ALL (full table scan), manca un indice e questo ha un impatto importante su quasi 3 milioni di righe.

Chiamate a API esterne senza timeout: se lo script attende una risposta da un servizio esterno che non risponde, blocca l’intero processo PHP. Impostare sempre un timeout esplicito nelle chiamate HTTP

$ch = curl_init();
curl_setopt($ch, CURLOPT_TIMEOUT, 10);   // massimo 10 secondi

Server sotto carico eccessivo

Una causa semplice e tra le più comuni degli errori 504 è la mancanza di risorse sul server upstream. Quando CPU, RAM o disco sono saturi, PHP-FPM impiega molto più tempo a elaborare le richieste, fino a superare il timeout di nginx. Possiamo verificare il carico del server con i seguenti comandi

# Monitoraggio processi e utilizzo CPU/RAM in tempo reale
top

# Versione avanzata di top, con navigazione interattiva e colori
htop

# Utilizzo disco (I/O wait alto è un segnale di disco saturo)
iostat -x 1 5

# Memoria disponibile
free -h

Se il server è costantemente al limite, le soluzioni sono:

  • Aumentare le risorse (RAM, CPU) del server
  • Ottimizzare PHP-FPM riducendo il numero di worker se la RAM è insufficiente
  • Implementare una cache (Redis, Memcached, FastCGI cache di Nginx) per ridurre le richieste che arrivano a PHP

Timeout nel backend HTTP (proxy verso Node.js, Apache, altro Nginx)

Se nginx opera come reverse proxy verso un backend HTTP anziché verso PHP-FPM direttamente, le direttive di timeout da modificare sono quelle del modulo proxy

location / {
    proxy_pass            http://127.0.0.1:3000;
    proxy_http_version    1.1;
    proxy_read_timeout    120s;
    proxy_send_timeout    120s;
    proxy_connect_timeout 10s;
    proxy_set_header      Host $host;
    proxy_set_header      X-Real-IP $remote_addr;
}

proxy_connect_timeout può rimanere basso (10-15s): se il backend non accetta la connessione in pochi secondi, il problema non è di timeout ma di disponibilità del servizio.

Problemi di rete o DNS tra Nginx e il backend

In ambienti con più server (microservizi, backend remoti), un errore 504 può essere causato da latenza di rete o da un DNS che risolve lentamente l’hostname del backend. Possiamo verificare la connettività con i seguenti comandi

# Test di connessione diretta al backend
curl -v --max-time 10 http://127.0.0.1:3000/

# Se il backend è su un altro host
ping backend-server.domain.local
traceroute backend-server.domain.local

Per verificare il tempo di risposta del DNS il comando è

time nslookup backend-server.domain.local

# Esempio di output
Server:         192.168.0.1
Address:        192.168.0.1#53

Name:   backend-server.domain.local
Address: 192.168.1.50

real    0m4.823s
user    0m0.012s
sys     0m0.008s

Se la risoluzione DNS è lenta, possiamo configurare nginx per usare un resolver esplicito e impostare un TTL breve

server {
    listen 443 ssl;
    server_name domain.local;

    resolver 192.168.0.1 valid=10s;

    location / {
        proxy_pass http://backend-server.domain.local;
        proxy_read_timeout 120s;
    }
}

Conclusione

L’errore 504 Gateway Timeout su nginx indica che il backend ha impiegato troppo tempo a rispondere o non ha risposto affatto entro il limite configurato. A differenza dell’errore 502, dove il canale di comunicazione è interrotto, nell’errore 504 il processo è avviato ma è troppo lento.

La prima azione da compiere è sempre leggere i log: access.log per capire su quali URL si manifesta il problema, error.log per identificare quale timeout si sta superando. Solo dopo aver individuato la causa possiamo intervenire sulla configurazione.