Risolvere l'errore nginx 502 Bad Gateway: cause e soluzioni
502 Bad Gateway significa che nginx non ha ricevuto dal backend nessuna risposta valida. Le cinque cause più frequenti, la riga giusta nel log degli errori e come dimostri che il fix funziona davvero.
Che cosa significa davvero "502 Bad Gateway"
Un 502 non arriva dalla tua applicazione, arriva da nginx. nginx ha accettato la richiesta, l'ha inoltrata a un backend (PHP-FPM, Node, Python, un altro server web) e da lì non ha ricevuto nessuna risposta utilizzabile. Ecco perché sulla pagina di errore non compare niente di utile.
Saper distinguere i codici vicini fa risparmiare molto tempo quando le cose si mettono male:
- 500 Internal Server Error: il backend ha risposto, ma la risposta era un errore. La causa sta nel codice dell'applicazione. Leggi il log dell'applicazione, non quello di nginx.
- 502 Bad Gateway: la connessione verso il backend non si è stabilita, oppure è caduta prima che arrivasse una risposta completa.
- 504 Gateway Time-out: la connessione reggeva, il backend è semplicemente rimasto zitto troppo a lungo e nginx ha perso la pazienza.
Questa distinzione è la leva più importante quando scadono i tempi di attesa, perché la stessa pagina lenta compare una volta come 502 e una volta come 504, a seconda di chi molla per primo. Ci torniamo più avanti.
La situazione dei pacchetti su Debian 13, Debian 12, Ubuntu 24.04 e Ubuntu 22.04
Di fronte agli errori 502 nginx si comporta allo stesso modo su tutti e quattro i sistemi, le direttive hanno gli stessi nomi. Le differenze stanno quasi per intero sul lato PHP, ed è esattamente da lì che nasce la maggior parte dei 502 dopo un cambio di distribuzione.
| Sistema | nginx | PHP | Nome del servizio | Socket |
| Debian 13 (Trixie) | 1.26.3 | 8.4 | php8.4-fpm | /run/php/php8.4-fpm.sock |
| Debian 12 (Bookworm) | 1.22.1 | 8.2 | php8.2-fpm | /run/php/php8.2-fpm.sock |
| Ubuntu 24.04 LTS | 1.24.0 | 8.3 | php8.3-fpm | /run/php/php8.3-fpm.sock |
| Ubuntu 22.04 LTS | 1.18.0 | 8.1 | php8.1-fpm | /run/php/php8.1-fpm.sock |
In tutti i comandi che seguono sostituisci il numero di versione con quello del tuo sistema. Tutti gli esempi presuppongono una shell root, altrimenti anteponi sudo. Quale versione sia installata lo scopri con un'occhiata ai binari FPM, anche se il servizio non parte affatto:
ls /usr/sbin/php-fpm*
ls /etc/php/
Prima di tutto il log degli errori: trovare la riga giusta
L'errore più frequente in fase di diagnosi è cercare nel log sbagliato. nginx ha un log degli errori globale e spesso uno proprio per ogni host virtuale. Quale file valga è scritto nella configurazione:
grep -Rn "error_log" /etc/nginx/nginx.conf /etc/nginx/sites-enabled/
Fai attenzione alla -R maiuscola. In /etc/nginx/sites-enabled/ su Debian e Ubuntu ci sono esclusivamente symlink verso sites-available, e GNU grep con la -r minuscola non segue nessun symlink durante la discesa ricorsiva. Con -rn ottieni quindi solo i risultati presi da nginx.conf, mentre la riga error_log propria del vhost resta invisibile: proprio quella che serve in caso di 502, perché il log globale non contiene l'errore FastCGI non appena il vhost devia altrove. Se preferisci restare su -r, passa direttamente a grep le directory di origine:
grep -rn "error_log" /etc/nginx/nginx.conf /etc/nginx/sites-available/ /etc/nginx/conf.d/
Senza un'indicazione propria nel blocco server tutto finisce in /var/log/nginx/error.log. La strada più affidabile verso la riga giusta passa da una registrazione dal vivo: tieni il log aperto in un terminale, scatena la richiesta in un secondo e guarda le righe che nel frattempo si aggiungono.
tail -f /var/log/nginx/error.log
In alternativa filtra sull'orario. nginx scrive l'ora locale nel formato 2026/07/26 09:14:22, non in UTC. Un confronto con un orologio in un altro fuso orario va storto regolarmente.
Una riga di 502 è sempre costruita secondo lo stesso schema. Esempio:
2026/07/26 09:14:22 [error] 812#812: *3 connect() to unix:/run/php/php8.2-fpm.sock
failed (2: No such file or directory) while connecting to upstream,
client: 203.0.113.7, server: example.com,
request: "GET /index.php HTTP/1.1",
upstream: "fastcgi://unix:/run/php/php8.2-fpm.sock:", host: "example.com"
Quattro elementi portano tutta l'informazione:
- La chiamata di sistema:
connect(),recv(),send().connect()significa che una connessione non si è mai stabilita.recv()significa che la connessione reggeva e poi è caduta. - Il numero di errore tra parentesi, vedi la tabella qui sotto. Questa è la diagnosi vera e propria.
- La fase:
while connecting to upstreamcontrowhile reading response header from upstream. Il primo è un problema di raggiungibilità, il secondo un problema di esecuzione o di crash. - Il campo
upstream:. Lì c'è il percorso o l'indirizzo che nginx ha realmente usato. Non quello che supponi di avere in configurazione, ma quello che è caricato e attivo.
| Messaggio | Significato | Sezione |
| 2: No such file or directory | Il file socket non esiste | Servizio morto o percorso sbagliato |
| 13: Permission denied | Il socket esiste, nginx non può accedervi | Permessi |
| 111: Connection refused | Niente è in ascolto su indirizzo e porta | Backend non raggiungibile |
| 110: Connection timed out | Nessuna risposta entro il termine | Timeout |
| 104: Connection reset by peer | Il processo backend è morto in mezzo alla richiesta | Crash e limiti |
| 11: Resource temporarily unavailable | Coda del socket piena | Crash e limiti |
La riga che riguarda 13: Permission denied in nginx porta spesso il livello [crit] invece di [error]. Chi filtra solo su [error] se la lascia sfuggire. Filtra piuttosto sul testo:
grep -n "upstream" /var/log/nginx/error.log
La seconda metà della verità sta nel log di PHP-FPM, per impostazione predefinita in /var/log/php8.2-fpm.log. In caso di crash e di limiti raggiunti lì trovi la motivazione, mentre nginx vede soltanto il sintomo.
Causa 1: PHP-FPM non è in esecuzione
Classico numero di errore 2. Controlla per prima cosa lo stato del servizio:
systemctl is-active php8.2-fpm
systemctl status php8.2-fpm --no-pager -l
is-active risponde con una parola sola, e per uno script basta. Se arriva inactive oppure failed, prendi il motivo dal journal, e fallo con una finestra temporale invece che con le ultime dieci righe:
journalctl -u php8.2-fpm --since "30 min ago" --no-pager
Molto spesso la causa è una configurazione del pool rotta, rimasta lì dopo un reload. FPM ha un proprio test di sintassi, che gira senza riavvio:
php-fpm8.2 -t
Errori di avvio tipici, riportati testualmente, e che cosa significano:
ERROR: [pool www] cannot get uid for user 'webuser': l'utente di sistema indicato inuser =non esiste più, per esempio dopo una migrazione.ERROR: unable to bind listening socket for address '/run/php/php8.2-fpm.sock': No such file or directory (2): manca la directory/run/php. Sta su un tmpfs e viene creata all'avvio del servizio. Se fai puntarelistena un percorso esterno, devi occuparti tu di far creare la directory.ERROR: An another FPM instance seems to already listen on ...: un processo rimasto da un riavvio andato male è ancora attaccato al socket.
Come capisci che è davvero risolto: non dal fatto che systemctl restart sia passato senza output. Un master FPM parte anche quando nessun processo di lavoro è in grado di accettare richieste. Quello che conta è che il socket sia visibile nel sistema e che FPM risponda su di esso.
ss -lx | grep php
Per il vero test di risposta abilita in /etc/php/8.2/fpm/pool.d/www.conf la riga ping.path = /ping, ricarica FPM e interroga il socket direttamente, scavalcando completamente nginx:
apt-get install -y libfcgi-bin
SCRIPT_NAME=/ping SCRIPT_FILENAME=/ping REQUEST_METHOD=GET \
cgi-fcgi -bind -connect /run/php/php8.2-fpm.sock
Se torna pong, il lato PHP è a posto e l'errore sta tra nginx e il socket. Se non torna niente, su nginx non serve nemmeno continuare a cercare.
Causa 2: percorso del socket sbagliato
Su Debian e Ubuntu è di gran lunga la causa più frequente, perché il socket porta nel nome la versione di PHP, la configurazione di nginx cabla quel nome in modo fisso e un aggiornamento di distribuzione separa le due cose.
In concreto: un aggiornamento da Debian 12 a Debian 13 porta PHP da 8.2 a 8.4. Il vecchio socket /run/php/php8.2-fpm.sock sparisce, nel file del vhost resta scritto. Il risultato è un 502 su ogni singola pagina PHP, subito dopo il riavvio. La stessa cosa capita passando da Ubuntu 22.04 a 24.04 (da 8.1 a 8.3).
Una seconda trappola sta nella configurazione di esempio fornita insieme al pacchetto. In /etc/nginx/sites-available/default c'è un blocco commentato il cui fastcgi_pass punta a una versione di PHP che non è più attuale da anni. Chi si limita a togliere il commento da quelle righe ha appena creato il 502.
Confronta i due lati. Quello che nginx vuole usare:
grep -Rn "fastcgi_pass" /etc/nginx/
Quello che FPM offre davvero:
grep -n "^listen *=" /etc/php/*/fpm/pool.d/*.conf
L'aggiunta di *= nel pattern di ricerca è voluta, richiede dopo listen un numero qualsiasi di spazi e poi un segno di uguale. Un semplice ^listen infatti prende anche listen.owner, listen.group e listen.mode, e la riga che cerchi si perde tra i risultati. Il carattere jolly /etc/php/*/ invece va bene così, copre ogni versione di PHP installata. E quello che esiste nel sistema in esecuzione:
ls -l /run/php/
Le tre uscite devono mostrare lo stesso percorso. Importante: grep -R su tutto /etc/nginx/, non solo sul singolo file che hai in sospetto. I frammenti agganciati tramite include sono un nascondiglio molto amato, così come i vecchi file in sites-available ancora attivi attraverso un symlink dimenticato in sites-enabled. Vale anche qui: solo la -R maiuscola segue questi symlink e ti mostra quindi quale file è davvero attivo.
ls -l /etc/nginx/sites-enabled/
Prima di cambiare qualcosa, fai una copia. Costa due secondi e nel dubbio ti risparmia un ripristino dal backup:
mkdir -p /root/backups
cp -a /etc/nginx/sites-available/default /root/backups/default.bak
Chi interviene più volte fa meglio ad aggiungere un orario (default.bak.$(date +%F-%H%M)), perché cp -a sovrascrive un .bak esistente senza dire niente.
Dopo la correzione prima si testa, poi si carica. reload invece di restart, così le connessioni esistenti non cadono:
nginx -t
systemctl reload nginx
Tieni presente: nginx -t verifica esclusivamente la sintassi. Un percorso di socket che non esiste vale come configurazione perfettamente valida. Un syntax is ok verde non è quindi una prova che il 502 sia sparito.
Causa 3: permessi sul socket
Numero di errore 13. Il socket c'è, solo che nginx non riesce ad aprirlo. Su Debian e Ubuntu nginx gira come utente www-data, e il pool predefinito di FPM crea il socket di conseguenza. Lo si vede nel file del pool:
grep -n "listen.owner\|listen.group\|listen.mode" /etc/php/*/fpm/pool.d/*.conf
Con listen.owner = www-data, listen.group = www-data e modo 0660 il meccanismo funziona senza interventi. Le cose possono andare storte in tre situazioni:
- Un pool proprio per ogni progetto. Se
useregroupvengono impostati su un utente di progetto,listen.groupdeve comunque restare un gruppo di cui nginx fa parte. È abitualelisten.owner = utenteprogettoinsieme alisten.group = www-data. - Socket fuori da /run. Non basta che sia accessibile il file socket: ogni directory lungo il percorso che porta fin lì ha bisogno del permesso di esecuzione per nginx. Un socket in una home directory con modo 0700 non è raggiungibile da nessuno tranne il proprietario.
- nginx con
usermodificato in/etc/nginx/nginx.conf.
Il sospetto lo confermi senza tirare a indovinare, provando l'accesso esattamente come l'utente che ne ha bisogno in esercizio:
id www-data
sudo -u www-data test -w /run/php/php8.2-fpm.sock && echo "accesso presente" || echo "nessun accesso"
Ancora più significativa è la chiamata a cgi-fcgi della sezione precedente, anch'essa preceduta da sudo -u www-data. Se FPM risponde come root ma non come www-data, la diagnosi è certa.
Imposta i valori nel file del pool, non con chmod sul file socket. Un chmod 666 regge esattamente fino al prossimo riavvio di FPM, poi FPM ricrea il socket con i permessi configurati e l'errore è di nuovo lì, di solito nel momento peggiore.
Su Debian e Ubuntu AppArmor è attivo. Un profilo per nginx viene fornito insieme al pacchetto e allo stato di consegna non è imposto, però può essere stato attivato da modelli di hardening. Se un messaggio 13 resta anche con permessi corretti, vale la pena guardare aa-status e journalctl -k | grep DENIED.
Causa 4: timeout con richieste lunghe
Qui il metodo pulito si separa dall'indovinare, perché la semplice scadenza del termine di nginx genera un 504, non un 502. Quando una richiesta lunga finisce con un 502, quasi sempre è stato PHP-FPM a togliere di mezzo prima il processo di lavoro, e nginx ha visto soltanto una connessione interrotta. Nel log compare allora tipicamente:
recv() failed (104: Connection reset by peer) while reading response header from upstream
Tre termini agiscono contemporaneamente, e il loro ordine decide il codice di stato:
max_execution_timeinphp.ini, valore predefinito 30 secondi in esercizio FPM. Conta solo il tempo di esecuzione dello script. L'attesa nelle chiamate di sistema, per esempio su una query al database bloccata, sotto Linux non viene conteggiata. Per questo motivo il valore non ti salva proprio nel caso in cui te lo aspetteresti.request_terminate_timeoutnel file del pool, disattivato allo stato di consegna. Termina il processo di lavoro in modo netto, qualunque sia la cosa su cui è bloccato. È il valore che produce i 502.fastcgi_read_timeoutin nginx, valore predefinito 60 secondi. Se scade, arriva un 504.
L'ordine utile è crescente dall'interno verso l'esterno, in modo che scatti sempre per primo lo strato ancora capace di produrre un messaggio di errore comprensibile. Per esempio 60, poi 75, poi 90 secondi. Nell'ordine inverso ottieni 502 al posto di errori PHP leggibili.
grep -rn "request_terminate_timeout" /etc/php/*/fpm/pool.d/*.conf
Quello che FPM ha tolto di mezzo sta scritto in chiaro nel suo log:
WARNING: [pool www] child 1234, script '/var/www/html/import.php'
(request: "POST /import.php") execution timed out (76.271849 sec), terminating
Prima di alzare i termini, fatti mostrare dove se ne va il tempo. FPM porta con sé un log apposito, che in caso di superamento scrive uno stack completo delle chiamate PHP. Da attivare nel file del pool:
slowlog = /var/log/php-fpm-slow.log
request_slowlog_timeout = 5s
Dopo un systemctl reload php8.2-fpm, alla prossima richiesta lenta lì trovi la funzione con il numero di riga che si blocca. Nella pratica in quattro casi su cinque si tratta di una query al database senza indice oppure di una chiamata a un'interfaccia di programmazione esterna senza un termine proprio. Alzare i termini allunga allora soltanto il tempo che passa fino all'errore e in più tiene occupati processi di lavoro.
Causa 5: backend non raggiungibile
Riguarda ogni backend contattato via TCP: FPM sulla porta 9000, un'applicazione Node, un servizio Java, un container. Il messaggio guida è il numero di errore 111.
connect() to 127.0.0.1:3000 failed (111: Connection refused) while connecting to upstream
Controlla prima di tutto se c'è qualcosa in ascolto, e soprattutto su che cosa:
ss -ltnp
Questo comando elenca esclusivamente socket TCP, e da qui nasce un equivoco molto diffuso: un pool PHP-FPM allo stato di consegna è in ascolto su un socket Unix sotto /run/php/ e in questa lista non compare affatto, anche se funziona perfettamente. Diventa visibile solo così:
ss -lxn | grep php-fpm
ls -l /run/php/
Per FPM quindi ss -ltnp è significativo solo se il pool è stato spostato deliberatamente su TCP con listen = 127.0.0.1:9000. Con backend Node, Java o in container è invece esattamente il comando giusto.
Tre punti critici che nelle guide si trovano di rado:
- localhost risolve prima su ::1. Se in nginx c'è
proxy_pass http://localhost:3000;ma l'applicazione è in ascolto solo su127.0.0.1, nginx prova l'indirizzo IPv6 e riceve "Connection refused". Il servizio gira, la porta è aperta, eppure arriva un 502. Soluzione: in nginx scrivi per esteso127.0.0.1, oppure fai ascoltare l'applicazione su entrambe le famiglie di indirizzi. - nginx risolve i nomi una volta sola, al caricamento. Se in
proxy_passc'è un nome host, nginx si annota l'indirizzo. Se il backend cambia indirizzo IP, per esempio un container appena riavviato, le richieste girano a vuoto fino al prossimoreload. - Firewall sulla via del ritorno. Con un backend su un altro server compare
113: No route to hostoppure un timeout invece di "Connection refused". Verifica conufw statuse con un test di connessione diretto dal server nginx.
Se viene usato un blocco upstream con più destinazioni, si aggiunge un messaggio a sé:
no live upstreams while connecting to upstream
Significa che nginx, dopo ripetuti tentativi falliti, ha tolto dal traffico tutte le destinazioni per la durata di fail_timeout. Anche dopo aver riparato il backend passa quindi altro tempo, fino alla scadenza di questo termine, prima che le richieste tornino a passare. Un systemctl reload nginx azzera subito questo stato.
Il 502 che non rientra in nessuna delle cinque cause
Due casi sembrano un guasto ma non lo sono, e proprio per questo costano un tempo superiore alla media.
Header di risposta troppo grande. L'applicazione funziona perfettamente, solo che singole richieste restituiscono 502:
upstream sent too big header while reading response header from upstream
A scatenarlo sono cookie grandi o attributi di sessione nelle intestazioni, il buffer di nginx è troppo piccolo. Nel blocco server o location:
fastcgi_buffer_size 32k;
fastcgi_buffers 8 16k;
fastcgi_busy_buffers_size 64k;
Con proxy_pass le direttive si chiamano proxy_buffer_size e proxy_buffers. È tipico che siano colpiti solo gli utenti che hanno effettuato l'accesso, mentre la pagina iniziale carica senza problemi.
Processi di lavoro esauriti. Sotto carico nel log di FPM compare:
WARNING: [pool www] server reached pm.max_children setting (5), consider raising it
Le nuove richieste aspettano allora nella coda del socket. Se anche quella è piena, nginx segnala 11: Resource temporarily unavailable. Prima di aumentare pm.max_children, fai due conti: RAM disponibile diviso il consumo reale di un processo di lavoro. Un valore troppo alto scambia i 502 con uno stato del sistema in cui la memoria finisce, e questo colpisce poi anche il database.
Crash. Righe con exited on signal 11 (SIGSEGV) indicano un'estensione PHP difettosa, spesso dopo un cambio di versione di PHP con moduli rimasti dalla versione precedente.
Se l'intervento non serve a niente: la via del ritorno
Due regole tengono piccolo il danno. Primo: una modifica alla volta, con una copia del file originale sotto /root/backups, mai nella directory web. Secondo: dopo ogni passo una controprova, invece di cambiare tre cose insieme e poi non sapere quale abbia aiutato.
Se dopo una modifica nginx si pianta del tutto, rimetti a posto la copia e ricarica:
cp -a /root/backups/default.bak /etc/nginx/sites-available/default
nginx -t
systemctl reload nginx
Se dopo un restart nginx non parte più, systemctl status nginx dice raramente abbastanza. Più significativo:
journalctl -u nginx --since "10 min ago" --no-pager
Il motivo più frequente di un restart fallito con una configurazione sintatticamente impeccabile è una porta 80 o 443 occupata, di solito da un processo del giro precedente. ss -ltnp | grep ':80' mostra il colpevole.
Come capisci che è davvero risolto
Un comando senza messaggio di errore non dimostra proprio niente. systemctl reload resta zitto anche quando nella sostanza non è cambiato nulla, e nginx -t verifica solo la sintassi. Solide sono queste quattro prove:
- Interroga il codice di stato direttamente sul server, così né una cache né un servizio a monte falsano il risultato:
Ci si aspetta 200, non 502. Con più host virtuali passa anche il nome:curl -sS -o /dev/null -w "%{http_code}\n" http://127.0.0.1/curl -H "Host: example.com" ... - Il log degli errori resta muto. Svuotalo prima del test con
truncate -s 0 /var/log/nginx/error.log, scatena diverse richieste, poi guardaci di nuovo dentro. Un file vuoto è la prova vera. - FPM risponde sul socket scavalcando nginx, tramite
cgi-fcgie comewww-data. Così la questione dei permessi e quella del percorso sono risolte in un colpo solo. - Un riavvio non cambia nulla. Il punto più importante e quello che si salta più spesso. Molte misure immediate (permessi impostati a mano, directory create manualmente sotto
/run, un servizio avviato ma non abilitato) non sopravvivono a un riavvio. Verificasystemctl is-enabled php8.2-fpm nginxe riavvia il server una volta in modo controllato, finché ci stai ancora davanti, invece di lasciarlo alla prossima finestra di manutenzione.
Sui server root KVM e sui server dedicati di KernelHost questo riavvio, accesso alla console compreso, lo esegui dall'area clienti, anche quando il servizio web in quel momento non è raggiungibile. I server si trovano nel datacenter maincubes di Francoforte sul Meno (TÜV TIER3+), collegati a una rete propria con protezione DDoS. Per approfondire: risolvere l'errore nginx 504 Gateway Time-out e calcolare correttamente pm.max_children di PHP-FPM.
Checklist rapida per le emergenze
tail -f /var/log/nginx/error.log, scatena la richiesta, annota il numero di errore.- Numero 2 o 111: il servizio gira, il percorso è giusto.
ss -lx | grep phpcontrogrep -Rn "fastcgi_pass" /etc/nginx/. - Numero 13: permessi nel file del pool, non con
chmod. - Numero 104 o 110: leggi il log di FPM e lo
slowlog, solo dopo si parla di termini. - Messaggio "too big header": aumenta le dimensioni dei buffer.
- Dopo il fix: svuota il log, testa di nuovo, riavvia il server una volta.
Domande frequenti
Perché ottengo un 502 e non un 504, se la pagina è semplicemente lenta?
Dopo l'aggiornamento a Debian 13 tutte le pagine PHP danno 502. Che cosa devo cambiare?
nginx -t basta come prova che l'errore è risolto?
Come trovo nel log degli errori la riga che appartiene al mio 502?
Il 502 compare solo per gli utenti che hanno effettuato l'accesso, la pagina iniziale carica normalmente. Da che cosa dipende?
Ho corretto i permessi del socket con chmod, dopo un riavvio l'errore è tornato. Perché?
2026 KernelHost GmbH. Tutti i diritti riservati. Questa guida è protetta dal diritto d'autore. La ripubblicazione su altri siti web, anche parziale o in forma modificata, non è consentita senza il nostro consenso scritto. Le citazioni con indicazione della fonte e un link sono le benvenute.

