Alle 09:12 il grafico dei 502 sale all’improvviso. Il log di Nginx mostra messaggi diversi: upstream timed out, connect() failed, e in alcuni casi SSL_do_handshake() failed. Il sito non è “giù” in modo totale. Va lento, poi cade solo sotto carico.
Questo articolo parte da quel sintomo, non da una configurazione teorica. L’obiettivo è isolare la causa reale e correggere Nginx senza cambiare dieci cose insieme.
Note: useremo Nginx come reverse proxy verso un upstream PHP-FPM o applicativo. Le stesse logiche valgono anche con Apache dietro proxy o bilanciatore.
Prerequisiti
Ti serve accesso al server, ai log di Nginx e alla configurazione del virtual host. È utile avere anche un secondo terminale per fare test mentre il traffico reale continua.
- Nginx 1.20 o superiore.
- Accesso a /var/log/nginx/error.log e ai log access.
- Possibilità di ricaricare la config con nginx -t e systemctl reload nginx.
- Un endpoint che risponde tramite upstream, ad esempio PHP-FPM, Node.js o un microservizio interno.
- Strumenti base: curl, grep, awk, ss, openssl.
Warning: non aumentare subito tutti i timeout. Se il problema è un upstream saturo, così nascondi solo il guasto.
Step 1: leggere il 502 nel log e capire quale lato sta fallendo
Il primo passo è distinguere tra errore di connessione, timeout di lettura e handshake TLS. Il 502 è un contenitore, non una diagnosi.
Controlla il log error con una finestra temporale stretta. Cerca il messaggio completo, non solo il codice HTTP.
grep -E 'upstream timed out|connect\(\) failed|SSL_do_handshake\(\) failed|502' /var/log/nginx/error.log | tail -n 30# Output:
2026/03/25 09:12:04 [error] 18433#18433: *981 upstream timed out (110: Connection timed out) while reading response header from upstream, client: 203.0.113.18, server: shop.example.com, request: "GET /checkout HTTP/2.0", upstream: "http://127.0.0.1:9000/checkout", host: "shop.example.com"
2026/03/25 09:12:11 [error] 18433#18433: *993 connect() failed (111: Connection refused) while connecting to upstream, client: 203.0.113.18, server: shop.example.com, request: "GET /api/cart HTTP/2.0", upstream: "http://127.0.0.1:9000/api/cart", host: "shop.example.com"Qui hai già due piste diverse. Il primo caso è lentezza del backend. Il secondo è backend non raggiungibile o processo morto.
Se il messaggio cita SSL_do_handshake(), il problema è tra Nginx e l’upstream HTTPS, non tra browser e sito.
Step 2: misurare tempi reali con access log e variabili upstream
Per capire dove si perde tempo, fai emergere i tempi nel log access. Senza numeri, i timeout sono solo ipotesi.
Aggiungi un formato log temporaneo con i tempi principali. Ti basta un file di test o una location dedicata.
log_format timed '$remote_addr - $host "$request" '
'status=$status rt=$request_time '
'uct=$upstream_connect_time uht=$upstream_header_time urt=$upstream_response_time '
'ua="$upstream_addr"';
access_log /var/log/nginx/access_timed.log timed;# Output:
203.0.113.18 - shop.example.com "GET /checkout HTTP/2.0" status=200 rt=4.812 uct=0.001 uht=4.807 urt=4.811 ua="127.0.0.1:9000"Se uct è basso ma uht è alto, il backend accetta la connessione ma impiega troppo a produrre header. Se uct cresce, hai saturazione, backlog pieno o DNS/proxy interni lenti.
Note: in carico reale, il valore più utile non è la media. Guarda il p95 o il p99, perché i 502 nascono quasi sempre nelle code.
Step 3: verificare se il limite è nel backend, non in Nginx
Quando Nginx aspetta troppo, la tentazione è alzare proxy_read_timeout. Prima verifica se l’upstream risponde davvero nei tempi richiesti.
Fai un test diretto verso l’upstream, bypassando Nginx. Se è PHP-FPM, controlla anche lo stato del pool. Se è HTTP interno, usa curl con tempi dettagliati.
curl -sS -o /dev/null -w 'dns=%{time_namelookup} connect=%{time_connect} ttfb=%{time_starttransfer} total=%{time_total}\n' http://127.0.0.1:9000/checkout# Output:
dns=0.000 connect=0.000 ttfb=4.706 total=4.709Se il tempo totale è già alto fuori da Nginx, il proxy non è il colpevole. Il collo di bottiglia è nell’applicazione, nel database o nelle risorse del worker.
Controlla anche il numero di connessioni aperte sull’upstream.
ss -antp | grep ':9000' | head# Output:
LISTEN 0 511 127.0.0.1:9000 0.0.0.0:* users:("php-fpm",pid=2217,fd=8)
ESTAB 0 0 127.0.0.1:9000 127.0.0.1:51234 users:("nginx",pid=18433,fd=28)Se vedi molte connessioni in attesa o un pool piccolo, il backend sta esaurendo capacità. Nginx può solo aspettare o fallire.
Step 4: correggere i timeout con criterio, non per istinto
Se il backend è sano ma lento sotto picco, devi allineare i timeout al comportamento reale. Il punto non è “metterli alti”. Il punto è evitare falsi negativi senza coprire i guasti.
Usa valori diversi per connessione, invio e lettura. Non impostare un unico numero ovunque.
location / {
proxy_pass http://app_backend;
proxy_connect_timeout 3s;
proxy_send_timeout 15s;
proxy_read_timeout 30s;
send_timeout 15s;
}# Output:
nginx: configuration file /etc/nginx/nginx.conf test is successfulWarning: se hai stream lunghi, web socket o export pesanti, i timeout standard possono essere troppo stretti. Ma cambia solo la location interessata.
In un sito e-commerce, ad esempio, la pagina checkout può tollerare 30 secondi. L’API carrello no. I timeout vanno separati per percorso, non globalizzati.
Step 5: ridurre il costo del TLS tra Nginx e client, e tra Nginx e upstream
Se il log mostra errori TLS o il CPU sale troppo in fase di handshake, il problema può essere il costo della negoziazione. Questo è frequente con molte connessioni brevi e HTTP/2.
Per il lato client, abilita session reuse e preferisci cipher moderni. Per il lato upstream HTTPS, verifica che non stia rifacendo handshake completi ad ogni richiesta.
ssl_protocols TLSv1.2 TLSv1.3;
ssl_session_cache shared:SSL:20m;
ssl_session_timeout 10m;
ssl_session_tickets off;
location /api/ {
proxy_pass https://app_backend;
proxy_ssl_server_name on;
proxy_ssl_protocols TLSv1.2 TLSv1.3;
}# Output:
nginx: configuration file /etc/nginx/nginx.conf test is successfulSe usi un upstream HTTPS interno, controlla anche il certificato e il nome server inviato da Nginx. Un mismatch SNI produce errori intermittenti che sembrano “casuali”.
Test rapido con OpenSSL:
openssl s_client -connect 127.0.0.1:9443 -servername app.internal.example -brief # Output:
Protocol version: TLSv1.3
Ciphersuite: TLS_AES_256_GCM_SHA384
Verification: OKStep 6: usare la cache dove serve davvero, senza servire dati sbagliati
Molti 502 sotto carico nascono perché ogni richiesta arriva all’upstream. Una cache ben messa riduce il numero di connessioni e stabilizza il picco.
La cache va bene per risposte pubbliche, frammenti statici e API idempotenti. Non va bene per contenuti personalizzati senza variazioni corrette.
proxy_cache_path /var/cache/nginx levels=1:2 keys_zone=appcache:100m max_size=2g inactive=30m use_temp_path=off;
location /public/ {
proxy_cache appcache;
proxy_cache_valid 200 10m;
proxy_cache_valid 404 1m;
add_header X-Cache-Status $upstream_cache_status always;
proxy_pass http://app_backend;
}# Output:
X-Cache-Status: MISSNel traffico reale, dopo qualche minuto dovresti vedere HIT sulle richieste ripetute. Se resta sempre MISS, la chiave cache è sbagliata o stai inviando header che la invalidano ogni volta.
Per verificare, controlla i response header e l’effetto di Set-Cookie, Cache-Control e Vary. Un solo header può annullare il caching.
Step 7: ricaricare senza interrompere il traffico e validare il fix
Dopo la modifica, fai sempre il test di sintassi e il reload. Non usare restart se non hai un motivo preciso.
nginx -t && systemctl reload nginx# Output:
nginx: the configuration file /etc/nginx/nginx.conf syntax is ok
nginx: configuration file /etc/nginx/nginx.conf test is successfulSubito dopo, ripeti i test sul percorso critico. Il controllo finale deve combinare log, tempi e risposta HTTP.
curl -I https://shop.example.com/checkout
curl -sS -o /dev/null -w 'code=%{http_code} total=%{time_total}\n' https://shop.example.com/checkout# Output:
HTTP/2 200
code=200 total=0.842Se il 502 sparisce ma il tempo resta alto, non hai risolto del tutto. Hai solo spostato il problema più avanti.
Verifica finale
La verifica buona non si ferma al singolo curl. Deve confermare che il comportamento regga sotto un piccolo burst.
- Controlla che nel log error non compaiano più upstream timed out nei minuti successivi.
- Verifica che $upstream_response_time resti sotto la soglia attesa nel p95.
- Fai 20-50 richieste consecutive al percorso critico e guarda la stabilità.
- Controlla che la cache mostri HIT dove previsto.
- Se usi TLS verso l’upstream, conferma che non ci siano nuovi errori di handshake.
Un test semplice di carico leggero può bastare per scoprire il problema residuo.
for i in $(seq 1 20); do curl -sS -o /dev/null -w '%{http_code} %{time_total}\n' https://shop.example.com/public/; done# Output:
200 0.081
200 0.079
200 0.076Troubleshooting
Errore: upstream timed out (110: Connection timed out) while reading response header from upstream
Causa: l’upstream accetta la richiesta ma non produce header in tempo.
Fix:
grep -n 'proxy_read_timeout\|fastcgi_read_timeout' /etc/nginx/sites-enabled/*
Se il backend è lento solo sotto picco, alza il timeout solo per quella location e misura il p95.
Errore: connect() failed (111: Connection refused) while connecting to upstream
Causa: il processo upstream non ascolta più, oppure il pool è saturo e il socket rifiuta connessioni.
Fix:
systemctl status php-fpm
ss -lntp | grep 9000
Riavvia o aumenta la capacità del backend, non Nginx.
Errore: SSL_do_handshake() failed (SSL: error:0A00010B:SSL routines::wrong version number)
Causa: Nginx parla HTTPS verso un upstream che risponde in HTTP, oppure SNI e certificato non combaciano.
Fix:
grep -n 'proxy_pass https\|proxy_ssl_server_name' /etc/nginx/sites-enabled/*
openssl s_client -connect 127.0.0.1:9443 -servername app.internal.example -brief
Allinea schema, porta e SNI. Se l’upstream è solo HTTP, usa proxy_pass http://.
Conclusione
Un 502 sotto carico non si risolve con un solo timeout più alto. Serve capire se il collo di bottiglia è il backend, il TLS, la cache o il routing verso l’upstream.
Il prossimo passo concreto è tenere attivo un log con $upstream_response_time per una settimana. Ti dirà prima di tutto quando il sistema degrada, non solo quando cade.
Commenti (0)
Nessun commento ancora.
Segnala contenuto
Elimina commento
Eliminare definitivamente questo commento?
L'azione non si può annullare.