1 Settembre 2026

La velocità di trasmissione dei file statici tramite HTTP/3 è di gran lunga inferiore a quella di HTTP/2 su NGINX

Un caso reale di troubleshooting su NGINX HTTP/3 mostra come buffer QUIC, RTT e virtual host possano trasformare una configurazione corretta in un collo di bottiglia.

NGINX-HTTP3-QUIC-Slow-Download

Tutto è iniziato con un ticket apparentemente semplice.

Un cliente ci segnalava che il download di alcuni archivi di grandi dimensioni dal proprio sito risultava inspiegabilmente lento quando effettuato tramite browser.

Non si trattava di una differenza marginale, di quelle che possono essere tranquillamente attribuite alla normale variabilità della rete.

Lo stesso archivio statico, di circa 144 MB, veniva scaricato dall’applicazione desktop del cliente in circa 17 secondi, mentre Chrome poteva impiegare oltre 90 secondi.

Da una parte avevamo quindi un throughput nell’ordine degli 8-9 MB/s.

Dall’altra appena 1,5-2 MB/s.

Il server come molti altri di nostri clienti era ospitato presso Hetzner, in Germania, ed eseguiva NGINX con HTTP/2 e HTTP/3 attivi.

Il primo pensiero, davanti a una differenza del genere, è quasi inevitabilmente rivolto alla rete.

MTU?

Path MTU Discovery?

Packet loss?

Firewall?

Filtering UDP?

Routing?

Un problema dell’infrastruttura del datacenter?

Una qualche forma di rate limiting?

Un bug del browser?

Tutte ipotesi assolutamente plausibili.

Il dettaglio che rendeva il caso particolarmente interessante era però un altro: l’applicazione desktop del cliente utilizzava HTTP/1.1 su TCP, mentre Chrome, trovando HTTP/3 annunciato dal server, utilizzava QUIC sopra UDP.

La domanda diventava quindi molto più specifica:

era veramente il protocollo HTTP/3 a rendere il trasferimento quasi dieci volte più lento?

Per rispondere bisognava smettere di confrontare applicazioni differenti e isolare una sola variabile.

Il cliente, fortunatamente, aveva già fatto esattamente questo.

Il cliente aveva già isolato HTTP/2 da HTTP/3

Il primo confronto tra l’applicazione desktop e Chrome era interessante, ma non ancora sufficiente per formulare una diagnosi.

Confrontare due applicazioni differenti significa infatti introdurre moltissime variabili.

L’applicazione Node.js può utilizzare buffer differenti, librerie TLS differenti, strategie differenti per la gestione delle connessioni e perfino meccanismi di download differenti rispetto a Chrome.

Il test realmente significativo doveva utilizzare:

  • lo stesso client;
  • lo stesso computer;
  • la stessa connessione Internet;
  • lo stesso binario di Chrome;
  • lo stesso hostname;
  • lo stesso server;
  • lo stesso file;
  • lo stesso endpoint HTTPS.

Doveva cambiare soltanto il protocollo.

Negli esempi di questo articolo per le dovute garanzie di privacy del nostro cliente, anonimizzeremo il dominio originale utilizzando:

example.com

e chiameremo il file:

example-package-0.2.3-win-x64.zip

L’URL utilizzato per i benchmark diventa quindi:

https://example.com/downloads/example-package-0.2.3-win-x64.zip

La cosa importante è che si trattava di un file completamente statico servito direttamente da NGINX.

Nessun PHP.

Nessun PHP-FPM.

Nessun WordPress.

Nessun database.

Nessun proxy verso un backend.

Nessun codice applicativo.

Questa caratteristica sarebbe diventata fondamentale perché permetteva di escludere immediatamente un’enorme quantità di possibili colli di bottiglia.

Primo test: disabilitare QUIC e utilizzare HTTP/2

Chrome e Chromium permettono di essere avviati con QUIC completamente disabilitato.

Lo switch è:

--disable-quic

Su Windows, per esempio:

"C:\Program Files\Google\Chrome\Application\chrome.exe" ^
--disable-quic ^
--user-data-dir="%TEMP%\chrome-h2-test" ^
"https://example.com/downloads/example-package-0.2.3-win-x64.zip"

L’utilizzo di:

--user-data-dir

è particolarmente utile durante questo genere di benchmark.

Chrome tende infatti a riutilizzare processi e profili già aperti. Avviare un profilo temporaneo differente permette di ridurre interferenze dovute a sessioni precedenti, cache, stato Alt-Svc e connessioni esistenti.

Su Linux l’equivalente potrebbe essere:

google-chrome 
--disable-quic 
--user-data-dir=/tmp/chrome-h2-test 
"https://example.com/downloads/example-package-0.2.3-win-x64.zip"

Con QUIC disabilitato, Chrome negoziava HTTP/2.

I risultati misurati dal cliente erano:

HTTP/2

7,65 MB/s
9,67 MB/s

Quindi sostanzialmente coerenti con la velocità osservata dal client HTTP/1.1, che saturava la banda della connessione ADSL consumer (noi in smartworking ad esempio utilizziamo una Fastweb da 100 Megabit).

Fin qui tutto normale.

Secondo test: forzare HTTP/3 sullo stesso Chrome

A questo punto veniva chiusa la precedente istanza e avviato nuovamente lo stesso binario Chrome, questa volta forzando QUIC verso lo specifico origin.

Lo switch utilizzato è:

--origin-to-force-quic-on=example.com:443

È importante fare attenzione alla sintassi.

A volte questo parametro viene rappresentato informalmente in documentazione o esempi come:

--origin-to-force-quic-on=[example.com:443]

ma le parentesi quadre indicano normalmente un placeholder e non vanno passate letteralmente.

La sintassi effettiva è:

--origin-to-force-quic-on=example.com:443

Per rendere il test ancora più esplicito possiamo utilizzare anche:

--enable-quic

ottenendo:

"C:\Program Files\Google\Chrome\Application\chrome.exe" ^
--enable-quic ^
--origin-to-force-quic-on=example.com:443 ^
--user-data-dir="%TEMP%\chrome-h3-test" ^
"https://example.com/downloads/example-package-0.2.3-win-x64.zip"

Su Linux:

google-chrome 
--enable-quic 
--origin-to-force-quic-on=example.com:443 
--user-data-dir=/tmp/chrome-h3-test 
"https://example.com/downloads/example-package-0.2.3-win-x64.zip"

Stesso computer.

Stesso Chrome.

Stesso server.

Stesso file.

Stessa connessione.

Questa volta però i risultati diventavano molto diversi e molto peggiori:

HTTP/3

1,64 MB/s
1,95 MB/s

Una differenza enorme, da 10 Megabyte al secondo (che sarebbero certamente aumentati con una connessione ADSL o fibra più veloce) ad una connessione strozzata a meno di 2 Megabyte al secondo.

Il test decisivo: H2 → H3 → H2

Il cliente aveva fatto anche un’altra cosa metodologicamente molto corretta.

Le prove non erano state eseguite in due blocchi separati, per esempio dieci benchmark HTTP/2 al mattino e dieci HTTP/3 mezz’ora più tardi.

Erano state interlacciate.

La sequenza era:

HTTP/2
HTTP/3
HTTP/2

I risultati:

--disable-quic

HTTP/2
7,65 MB/s
9,67 MB/s

poi:

--enable-quic
--origin-to-force-quic-on=example.com:443

HTTP/3
1,64 MB/s
1,95 MB/s

e subito dopo:

--disable-quic

HTTP/2
7,55 MB/s
9,68 MB/s

In forma ancora più evidente:

HTTP/2    ~8-10 MB/s

HTTP/3    ~1,5-2 MB/s

HTTP/2    ~8-10 MB/s

Questa sequenza iniziava a rendere poco credibile la spiegazione basata su una generica congestione Internet.

Sarebbe infatti necessario supporre che la connessione diventasse improvvisamente cinque o sei volte più lenta esattamente quando veniva attivato QUIC, per poi tornare immediatamente veloce quando si ritornava a HTTP/2.

Possibile in teoria.

Molto improbabile quando il risultato è riproducibile.

Verificare il protocollo in Chrome DevTools

Quando si eseguono benchmark di questo tipo è comunque importante non fidarsi ciecamente dei flag utilizzati per avviare il browser.

Chrome DevTools permette di visualizzare il protocollo realmente utilizzato.

chrome_enable_protocol

Basta aprire:

F12
→ Network

e abilitare la colonna:

Protocol

Nel primo test bisogna verificare la presenza di:

h2

mentre nel secondo:

h3

Questo controllo evita di interpretare come HTTP/3 una richiesta che, per qualsiasi ragione, abbia effettuato fallback verso HTTP/2.

Prima ipotesi: un problema UDP sulla rete Hetzner ?

HTTP/2 utilizza TCP.

HTTP/3 utilizza QUIC sopra UDP.

Era quindi naturale iniziare a indagare tutto ciò che avrebbe potuto penalizzare UDP lasciando sostanzialmente inalterato TCP.

Il server era ospitato sull’infrastruttura Hetzner in Germania e abbiamo iniziato a prendere in considerazione un eventuale comportamento specifico del percorso di rete.

Tra le prime ipotesi:

  • packet loss UDP;
  • filtering UDP;
  • UDP policing;
  • routing differente;
  • problemi su firewall intermedi;
  • code o shaping differenti per UDP e TCP.

Una piccola quantità di packet loss può incidere in maniera significativa sul congestion control.

HTTP/3 non è semplicemente “HTTP/2 trasportato sopra UDP”: QUIC implementa direttamente molte funzionalità che tradizionalmente appartengono allo stack TCP, inclusi acknowledgements, controllo di congestione e ritrasmissioni.

Il comportamento di un flusso QUIC in presenza di loss può quindi essere sensibilmente differente.

Seconda ipotesi: MTU e Path MTU Discovery

Subito dopo abbiamo considerato uno dei classici problemi delle connessioni UDP: la MTU.

Un percorso con MTU inferiore al previsto, un tunnel intermedio o una gestione non corretta dei messaggi ICMP Packet Too Big possono provocare comportamenti difficili da diagnosticare.

TCP dispone da decenni di un ecosistema molto maturo intorno a MSS e Path MTU.

QUIC deve invece gestire datagrammi UDP e packetizzazione secondo dinamiche proprie.

Sono quindi diventati sospetti:

MTU
PMTU
ICMP filtering
UDP fragmentation
tunnel
VLAN/VXLAN

Durante una diagnosi del genere può essere utile osservare direttamente il traffico:

tcpdump -ni any udp port 443

e confrontarlo con:

tcpdump -ni any tcp port 443

Ma anche qui mancava qualcosa.

Il throughput HTTP/3 non sembrava semplicemente “instabile”.

Sembrava quasi fermarsi sistematicamente intorno a un valore preciso.

Socket buffer e statistiche UDP del kernel

La ricerca è quindi proseguita nello stack Linux.

Un server HTTP/3 ad alte prestazioni dipende anche dalla corretta gestione dei socket UDP.

Tra i primi controlli:

netstat -su

oppure:

nstat -az

cercando valori quali:

UdpInErrors
UdpRcvbufErrors
UdpSndbufErrors

e successivamente i buffer del kernel:

sysctl net.core.rmem_max
sysctl net.core.wmem_max
sysctl net.core.rmem_default
sysctl net.core.wmem_default

Tutte ipotesi tecnicamente valide.

Ma continuava a esserci quel numero.

Circa 1,6 MB/s.

Il numero che iniziava a sembrare troppo preciso: 64 KB

Durante l’analisi abbiamo iniziato a guardare più attentamente la configurazione HTTP/3 di NGINX.

Esiste una direttiva chiamata:

http3_stream_buffer_size

La documentazione ufficiale la descrive come la dimensione del buffer utilizzato per leggere e scrivere gli stream QUIC.

Il default è:

http3_stream_buffer_size 64k;

e la direttiva può essere impostata nei contesti:

http
server

Il valore di 64 KB diventava immediatamente interessante.

Un maintainer NGINX ci ha spiegato che NGINX limita la quantità di byte non acknowledged su un singolo stream HTTP/3 in funzione di http3_stream_buffer_size.

http3_stream_buffer_size NGINX

In termini pratici questo può trasformarsi, in determinate condizioni, in un limite nell’ordine di:

buffer / RTT

Immaginiamo un RTT di circa:

40 ms

Con il buffer predefinito:

64 KB

otteniamo:

64 KB / 0,040 s ≈ 1,6 MB/s

Ed era esattamente l’ordine di grandezza dei nostri benchmark:

1,64 MB/s
1,95 MB/s

A quel punto il problema smetteva di sembrare casuale.

Il concetto di Bandwidth-Delay Product

Il principio è lo stesso che chi lavora da anni con TCP conosce attraverso il Bandwidth-Delay Product.

Se vogliamo mantenere una certa quantità di banda costantemente occupata dobbiamo avere abbastanza dati “in volo” durante il tempo necessario perché gli acknowledgement tornino al mittente.

Supponiamo di voler raggiungere:

10 MB/s

con:

RTT = 40 ms

Il BDP è:

10 MB/s × 0,040 s

ovvero:

0,4 MB

circa:

400 KB

Un buffer da 64 KB è quindi molto inferiore alla quantità di dati che dovrebbe poter rimanere contemporaneamente in volo per saturare quel percorso.

Non a caso il suggerimento ricevuto era di provare un valore nell’ordine di:

http3_stream_buffer_size 400k;

o, più comodamente:

http3_stream_buffer_size 512k;

Per avere margine potevamo utilizzare:

http3_stream_buffer_size 1m;

L’importanza del BDP nel dimensionamento di finestre e buffer su percorsi ad alta banda e latenza non è nuova: è uno dei principi fondamentali del throughput sulle reti WAN.

C’era inoltre un dettaglio curioso nella documentazione tecnica pubblicata dalla stessa NGINX: nei propri benchmark delle performance QUIC con RTT e BDP elevati, NGINX ha utilizzato:

http3_stream_buffer_size 50m;

quindi un valore enormemente superiore al default di 64 KB.

Un problema già visto da altri

Nel frattempo avevamo trovato anche una issue pubblica NGINX estremamente simile, la #1498 ( https://github.com/nginx/nginx/issues/1498), una issue relativamente recente ed attuale tenendo conto che siamo il 1° Settembre 2026 e la issue è stata aperta il 23 Giugno 2026, circa 2 mesi fa.

In quel caso veniva servito direttamente da NGINX un file statico da 1 GB.

I risultati erano circa:

HTTP/2    ~30 MB/s
HTTP/3    ~3,2 MB/s

Stesso server, stesso network path, nessun upstream applicativo.

Anche lì erano stati verificati UDP, GSO e altri aspetti della rete senza trovare una spiegazione immediata. La issue risultava in analisi da parte del progetto NGINX.

Il parallelismo con il nostro caso era evidente.

Il nostro rapporto era circa:

9 MB/s vs 1,8 MB/s

quello della issue:

30 MB/s vs 3,2 MB/s

Non era necessariamente lo stesso bug, ma certamente la stessa classe di fenomeno.

Modifichiamo http3_stream_buffer_size

A quel punto sembrava di aver trovato la strada giusta.

La configurazione del virtual host principale conteneva, in forma semplificata:

server {
listen 192.0.2.10:443 quic reuseport;
listen 192.0.2.10:443 ssl http2;

```
server_name example.com;

...

quic_gso on;
http3_stream_buffer_size 16m;

...
```

}

Avevamo utilizzato volutamente:

16m

un valore enormemente superiore ai 400-500 KB teoricamente necessari.

Non perché 16 MB fossero il valore corretto da utilizzare in produzione, ma perché volevamo una prova quasi binaria.

Se il problema fosse stato il buffer, passando da:

64 KB

a:

16 MB

il cambiamento avrebbe dovuto essere evidente.

Abbiamo eseguito:

nginx -t

seguito dal reload.

Nuovo benchmark.

HTTP/3 continuava ad andare praticamente alla stessa velocità.

Circa dieci volte più lentamente di HTTP/2.

Sembrava che l’ipotesi fosse sbagliata.

E invece era proprio in quel momento che il caso diventava più interessante.

Perché 16m non cambiava nulla?

La configurazione conteneva due virtual host HTTPS sulla stessa coppia IP:porta.

In forma estremamente semplificata:

server {
listen 192.0.2.10:443 quic;
listen 192.0.2.10:443 ssl http2;

```
server_name www.example.com;

...
```

}

e successivamente:

server {
listen 192.0.2.10:443 quic reuseport;
listen 192.0.2.10:443 ssl http2;

```
server_name example.com;

http3_stream_buffer_size 16m;

...
```

}

La direttiva:

http3_stream_buffer_size 16m;

era presente esclusivamente nel secondo server {}.

Normalmente, ragionando in termini di HTTP tradizionale e virtual hosting, viene spontaneo pensare:

La richiesta arriva per example.com, quindi NGINX seleziona il server block di example.com e utilizza i suoi parametri.

Ma QUIC introduce un problema temporale.

La connessione deve essere inizializzata prima che esista una normale richiesta HTTP completamente processata.

Ed è qui che abbiamo deciso di smettere di ragionare soltanto sulla documentazione e andare direttamente a leggere il codice sorgente di NGINX.

Leggere il sorgente NGINX: dove nasce realmente la connessione HTTP/3

La funzione interessante si trova in:

src/http/ngx_http_request.c

all’interno di:

ngx_http_init_connection()

Questa è una funzione molto importante perché rappresenta uno dei primi punti nei quali NGINX associa una nuova connessione al sottosistema HTTP.

Il codice determina innanzitutto la configurazione relativa all’indirizzo e alla porta sulla quale la connessione è arrivata.

A un certo punto troviamo una riga particolarmente significativa:

hc->conf_ctx = hc->addr_conf->default_server->ctx;

Il commento immediatamente precedente nel sorgente NGINX è altrettanto esplicito: in questa fase viene utilizzata la configurazione del default server per quella coppia address:port.

Questo significa che, in quel preciso momento, NGINX non è ancora partito dalla configurazione del virtual host che intuitivamente associamo al nome richiesto.

Sta lavorando con il contesto del:

default_server

associato al listener.

Fin qui potrebbe ancora non essere un problema.

La parte decisiva arriva poche istruzioni dopo.

Sempre nella stessa funzione troviamo la gestione specifica HTTP/3:

if (hc->addr_conf->quic) {
ngx_http_v3_init_stream(c);
return;
}

La sequenza logica diventa quindi:

NGINX QUIC HTTP3 Connection Flow

 

Questo return è particolarmente importante.

NGINX non prosegue lungo il normale percorso di attesa e parsing della richiesta HTTP.

Se la connessione è QUIC, entra immediatamente nell’inizializzazione HTTP/3.

Entriamo dentro ngx_http_v3_init_stream()

A questo punto abbiamo seguito il flusso nel file:

src/http/v3/ngx_http_v3_request.c

La funzione:

ngx_http_v3_init_stream()

riceve la connessione appena inizializzata.

All’interno troviamo un altro passaggio decisivo:

h3scf = ngx_http_get_module_srv_conf(hc->conf_ctx,
ngx_http_v3_module);

e poco dopo:

ngx_quic_run(c, &h3scf->quic);

Proviamo a tradurlo senza perdersi nei dettagli dell’API interna di NGINX.

h3scf è la server configuration del modulo HTTP/3.

Da dove viene recuperata?

Da:

hc->conf_ctx

Ma pochi istanti prima quel hc->conf_ctx era stato impostato a:

default_server->ctx

Quindi il percorso diventa:

NGINX HTTP3 nginx_http_v3_module diagram

È qui che la nostra configurazione ha finalmente iniziato ad avere senso.

Il parametro:

http3_stream_buffer_size 16m;

era presente nel secondo virtual host.

Ma la connessione QUIC veniva inizializzata utilizzando il contesto disponibile prima della normale selezione del virtual host HTTP basata sulla richiesta.

Il sorgente mostra inoltre che, quando vengono successivamente creati gli stream QUIC figli, NGINX può ereditare il contesto della connessione HTTP/3 parent dopo la gestione del server name; ma a quel punto il trasporto QUIC iniziale è già stato avviato tramite ngx_quic_run().

Ed era proprio questo il dettaglio che stavamo cercando.

Andiamo ancora più in profondità: dove viene definito il default da 64 KB?

A questo punto mancava un’ultima verifica.

Dove viene realmente impostato:

65536

come valore predefinito?

La risposta è in:

src/http/v3/ngx_http_v3_module.c

La definizione della direttiva associa:

http3_stream_buffer_size

direttamente a:

quic.stream_buffer_size

all’interno della configurazione HTTP/3.

Durante la creazione della configurazione il valore viene inizialmente marcato come non impostato.

Successivamente, nella funzione di merge delle configurazioni, troviamo il valore:

65536

come default finale.

Ovvero:

65536 byte = 64 KiB

Il sorgente confermava quindi esattamente ciò che riportava la documentazione:

http3_stream_buffer_size 64k;

Ma soprattutto mostrava che quel valore finiva direttamente nella struttura:

quic.stream_buffer_size

che veniva successivamente passata al motore QUIC.

Avevamo finalmente ricostruito tutto il percorso.

Semplificando molto il percorso completo nel codice NGINX è :

Percorso completo codice HTTP3 NGINX

 

La cosa importante non è tanto conoscere a memoria queste funzioni.

È comprendere l’ordine temporale degli eventi.

Con HTTP/3 non possiamo sempre ragionare come se prima arrivasse una normale richiesta HTTP completa e soltanto dopo venisse selezionato tutto il virtual host.

Alcuni parametri necessari al trasporto QUIC devono essere disponibili molto prima.

La soluzione: spostare il parametro nel contesto http

A questo punto abbiamo modificato la configurazione.

Invece di avere:

server {
server_name example.com;

```
http3_stream_buffer_size 16m;
```

}

abbiamo spostato il parametro nel blocco globale:

http {

```
http3_stream_buffer_size 1m;

...
```

}

La direttiva supporta ufficialmente entrambi i contesti:

http
server

Utilizzarla a livello http {} aveva però un vantaggio fondamentale nel nostro scenario: tutti i virtual server ereditavano lo stesso valore e non esisteva più alcuna ambiguità durante l’inizializzazione QUIC associata al listener.

Abbiamo verificato la configurazione effettivamente caricata con:

nginx -T 2>&1 | grep -n http3_stream_buffer_size

quindi:

nginx -t

e:

systemctl reload nginx

Nuovo benchmark.

Questa volta il risultato è cambiato immediatamente.

HTTP/3 non era più inchiodato intorno agli 1,5-2 MB/s.

Il throughput tornava comparabile a quello ottenuto attraverso HTTP/2.

Il problema era risolto.

Dowload static file chrome nginx quic http3 slow

Anche Chrome riusciva a saturare la connessione della nostra connessione Fastweb a 100 Megabit.

 

 

Perché la prima modifica sembrava corretta ma non lo era

Questa è probabilmente la parte più interessante dell’intero caso.

La configurazione:

server {
server_name example.com;

```
http3_stream_buffer_size 16m;
```

}

non era sintatticamente sbagliata.

NGINX la accettava.

nginx -t restituiva correttamente:

syntax is ok
test is successful

La direttiva è ufficialmente permessa nel contesto server.

Eppure, nella particolare topologia con più virtual host QUIC sullo stesso socket, non stava influenzando la fase della connessione che ci interessava.

Non era quindi un errore di sintassi.

Era un errore di comprensione del lifecycle della connessione.

Ed è esattamente qui che leggere il codice sorgente può fare la differenza.

Non impostate 16 MB senza motivo

Nel nostro test avevamo utilizzato:

http3_stream_buffer_size 16m;

esclusivamente come esperimento.

Non significa che 16 MB rappresentino il valore ottimale.

Il parametro dovrebbe idealmente essere dimensionato rispetto al bandwidth-delay product del tipo di connessioni che vogliamo servire.

Con:

10 MB/s
40 ms RTT

abbiamo:

10 × 0,040 = 0,4 MB

quindi circa 400 KB.

Un valore come:

http3_stream_buffer_size 512k;

potrebbe già essere sufficiente.

Oppure si può utilizzare un margine:

http3_stream_buffer_size 1m;

Naturalmente non esiste un numero universalmente corretto.

Consideriamo per esempio un server che debba sostenere:

50 MB/s

verso client con:

80 ms RTT

Il BDP diventa:

50 × 0,080 = 4 MB

In quel caso un buffer da 512 KB sarebbe nuovamente insufficiente per sfruttare completamente il percorso.

Il valore corretto dipende quindi da:

throughput desiderato
×
RTT atteso

con un opportuno margine operativo.

Una checklist diagnostica pratica

Se vi trovate davanti a HTTP/3 molto più lento di HTTP/2, non partirei modificando dieci parametri contemporaneamente.

La sequenza che questo caso ci ha suggerito è più simile alla seguente.

Prima isolare il protocollo:

H2 → H3 → H2

Poi verificare che il file sia realmente statico.

Successivamente controllare RTT:

ping example.com

e confrontarlo con il throughput osservato.

Se, per esempio, avete:

RTT ≈ 40 ms
HTTP/3 ≈ 1,5 MB/s

il default da 64 KB diventa immediatamente sospetto.

Controllare quindi:

http3_stream_buffer_size

e soprattutto dove è stato configurato.

Con più virtual host QUIC sullo stesso socket, verificare quale sia il default server e considerare seriamente di impostare i parametri QUIC condivisi nel contesto:

http {}

Soltanto se il buffer non modifica il comportamento passerei alle altre possibili ipotesi:

MTU / PMTU
UDP packet loss
firewall
UDP policing
routing
GSO
GRO
socket buffer
NIC
kernel
congestion control

Questa sequenza evita di perdere ore nel networking quando il collo di bottiglia è invece nella configurazione del trasporto HTTP/3.

HTTP/3 non è automaticamente sempre più veloce di HTTP/2

Uno degli errori più frequenti nell’ottimizzazione web consiste nell’associare la versione più recente di un protocollo a un incremento automatico delle prestazioni.

HTTP/3 offre vantaggi tecnologici importanti.

QUIC evita l’head-of-line blocking tra stream a livello di trasporto tipico di TCP, integra il trasporto sicuro con TLS, consente connection migration ed è stato progettato pensando anche a reti moderne, mobili e soggette a variazioni.

Ma nessuno di questi vantaggi significa:

HTTP/3 > HTTP/2

in ogni singolo benchmark.

Il modulo HTTP/3 di NGINX viene ancora esplicitamente descritto dalla documentazione ufficiale come experimental, con il relativo “caveat emptor”.

Inoltre la performance di QUIC dipende da elementi che nel mondo HTTP/2 potevamo spesso ignorare:

  • buffer per stream;
  • congestion control QUIC;
  • RTT;
  • packet loss;
  • UDP offloading;
  • comportamento del kernel;
  • dimensionamento del BDP;
  • implementazione specifica del server.

NGINX stesso ha pubblicato benchmark nei quali il tuning del congestion control QUIC produce differenze molto importanti al variare di RTT e BDP, utilizzando per quelle prove un http3_stream_buffer_size molto superiore al default.

Il valore del debugging per esclusione

Questo caso è anche un buon esempio di come dovrebbe essere affrontato un problema di performance.

All’inizio avevamo una descrizione molto generica:

“Da browser i file vengono scaricati lentamente.”

Una segnalazione così potrebbe portare in decine di direzioni.

La prima trasformazione importante è stata:

“Chrome è lento, l’applicazione desktop no.”

Poi:

“HTTP/1.1 e HTTP/2 sono veloci, HTTP/3 è lento.”

Poi ancora:

“Il file è statico e il comportamento segue esclusivamente il protocollo.”

Infine:

“Il throughput di HTTP/3 corrisponde sorprendentemente bene a 64 KB per RTT.”

Ogni test eliminava un pezzo del problema.

È la differenza tra cambiare configurazioni casualmente e fare diagnosi.

E soprattutto: non fidarsi solamente della configurazione che vediamo

Il secondo insegnamento è ancora più interessante.

Avevamo impostato:

http3_stream_buffer_size 16m;

Guardando il virtual host sembrava tutto corretto.

Il dominio richiesto era quello.

La direttiva era presente.

NGINX aveva effettuato il reload senza errori.

Ma il risultato non cambiava.

A quel punto avremmo potuto concludere:

“Quindi http3_stream_buffer_size non c’entra.”

Sarebbe stata una conclusione sbagliata.

Leggere:

src/http/ngx_http_request.c

e:

src/http/v3/ngx_http_v3_request.c

ha mostrato qualcosa che la semplice lettura di nginx.conf non rende evidente.

La sequenza era:

default server del socket
↓
inizializzazione HTTP/3
↓
lettura della configurazione QUIC
↓
ngx_quic_run()

Solo dopo entrano pienamente in gioco gli ulteriori meccanismi di associazione del server name e degli stream.

Il problema non era quindi che il valore fosse troppo basso.

Era che il valore che pensavamo di aver modificato non era quello utilizzato nel momento decisivo dell’inizializzazione della connessione QUIC.

La configurazione finale

In un’infrastruttura con più virtual host HTTP/3 sulla stessa porta, per parametri che vogliamo rendere comuni preferiamo quindi una configurazione del genere:

http {

```
http3_stream_buffer_size 1m;

...

server {
    listen 192.0.2.10:443 quic reuseport;
    listen 192.0.2.10:443 ssl;

    server_name www.example.com;

    ...
}

server {
    listen 192.0.2.10:443 quic reuseport;
    listen 192.0.2.10:443 ssl;

    server_name example.com;

    ...
}
```

}

Il valore 1m è soltanto esemplificativo e va dimensionato in funzione del caso reale.

La cosa fondamentale è che il parametro sia disponibile coerentemente al momento dell’inizializzazione QUIC.

Lo stesso ragionamento merita attenzione anche per altre direttive di server configuration QUIC, come quic_gso, perché nel sorgente anch’essa è memorizzata nella struttura di configurazione QUIC e il default è off.

Non significa che ogni direttiva debba obbligatoriamente essere spostata nel blocco globale.

Significa invece che, quando diversi virtual host condividono lo stesso listener QUIC, bisogna comprendere quali informazioni vengano necessitate durante le primissime fasi della connessione.

Conclusioni

Siamo partiti da un cliente che scaricava un archivio statico a circa:

1,5-2 MB/s

con Chrome.

La stessa risorsa attraverso HTTP/1.1 o HTTP/2 raggiungeva:

8-10 MB/s

Abbiamo inizialmente sospettato:

MTU
PMTU
firewall
routing UDP
packet loss
socket buffer
GSO
NIC
kernel
Hetzner

Tutte ipotesi perfettamente legittime.

Il test interlacciato eseguito con lo stesso Chrome aveva però dimostrato qualcosa di estremamente preciso:

H2 veloce
H3 lento
H2 nuovamente veloce

Il throughput HTTP/3 era inoltre sorprendentemente compatibile con il limite prodotto da:

64 KB / RTT

La documentazione NGINX confermava che:

http3_stream_buffer_size 64k;

è il valore predefinito.

Il codice sorgente confermava a sua volta che quel valore viene memorizzato nella configurazione QUIC e che il default implementato durante il merge è effettivamente 65536 byte.

Ma il passaggio realmente decisivo è arrivato analizzando il lifecycle della connessione.

In ngx_http_init_connection() NGINX parte dal default_server associato alla coppia address:port e, quando riconosce un listener QUIC, richiama immediatamente ngx_http_v3_init_stream().

Dentro quest’ultima funzione viene recuperata la configurazione HTTP/3 dal contesto disponibile e la struttura QUIC viene passata a ngx_quic_run().

Ecco perché impostare un enorme:

http3_stream_buffer_size 16m;

soltanto nel secondo virtual host non aveva cambiato il risultato.

Spostando invece la configurazione nel contesto globale:

http {
http3_stream_buffer_size 1m;
}

il limite è scomparso e HTTP/3 ha iniziato finalmente a trasferire il file a una velocità comparabile a HTTP/2.

Il caso è interessante non perché insegni semplicemente ad aumentare un buffer.

La vera lezione è un’altra.

Una configurazione sintatticamente corretta non è necessariamente applicata nella fase del protocollo nella quale immaginiamo che venga applicata.

Quando si lavora con tecnologie come QUIC, TLS, HTTP/2 e HTTP/3, conoscere soltanto le direttive di configurazione può non essere sufficiente.

A volte bisogna seguire la connessione fino dentro il sorgente.

Ed è proprio lì, tra un:

default_server->ctx

e una chiamata a:

ngx_quic_run()

che abbiamo trovato la differenza tra un download da un minuto e mezzo e uno da pochi secondi.

Hai dei dubbi? Non sai da dove iniziare? Contattaci !

Abbiamo tutte le risposte alle tue domande per aiutarti nella giusta scelta.

Chatta con noi

Chatta direttamente con il nostro supporto prevendita.

0256569681

Contattaci telefonicamente negli orari d’ufficio 9:30 – 19:30

Contattaci online

Apri una richiesta direttamente nell’area dei contatti.

DISCLAIMER, Note Legali e Copyright. Red Hat, Inc. detiene i diritti su Red Hat®, RHEL®, RedHat Linux®, e CentOS®; AlmaLinux™ è un marchio di AlmaLinux OS Foundation; Rocky Linux® è un marchio registrato di Rocky Linux Foundation; SUSE® è un marchio registrato di SUSE LLC; Canonical Ltd. detiene i diritti su Ubuntu®; Software in the Public Interest, Inc. detiene i diritti su Debian®; Linus Torvalds detiene i diritti su Linux®; FreeBSD® è un marchio registrato di The FreeBSD Foundation; NetBSD® è un marchio registrato di The NetBSD Foundation; OpenBSD® è un marchio registrato di Theo de Raadt; Oracle Corporation detiene i diritti su Oracle®, MySQL®, MyRocks®, VirtualBox® e ZFS®; Percona® è un marchio registrato di Percona LLC; MariaDB® è un marchio registrato di MariaDB Corporation Ab; PostgreSQL® è un marchio registrato di PostgreSQL Global Development Group; SQLite® è un marchio registrato di Hipp, Wyrick & Company, Inc.; KeyDB® è un marchio registrato di EQ Alpha Technology Ltd.; Typesense® è un marchio registrato di Typesense Inc.; REDIS® è un marchio registrato di Redis Labs Ltd; F5 Networks, Inc. detiene i diritti su NGINX® e NGINX Plus®; Varnish® è un marchio registrato di Varnish Software AB; HAProxy® è un marchio registrato di HAProxy Technologies LLC; Traefik® è un marchio registrato di Traefik Labs; Envoy® è un marchio registrato di CNCF; Adobe Inc. detiene i diritti su Magento®; PrestaShop® è un marchio registrato di PrestaShop SA; OpenCart® è un marchio registrato di OpenCart Limited; Automattic Inc. detiene i diritti su WordPress®, WooCommerce®, e JetPack®; Open Source Matters, Inc. detiene i diritti su Joomla®; Dries Buytaert detiene i diritti su Drupal®; Shopify® è un marchio registrato di Shopify Inc.; BigCommerce® è un marchio registrato di BigCommerce Pty. Ltd.; TYPO3® è un marchio registrato di TYPO3 Association; Ghost® è un marchio registrato di Ghost Foundation; Amazon Web Services, Inc. detiene i diritti su AWS® e Amazon SES®; Google LLC detiene i diritti su Google Cloud™, Chrome™, e Google Kubernetes Engine™; Alibaba Cloud® è un marchio registrato di Alibaba Group Holding Limited; DigitalOcean® è un marchio registrato di DigitalOcean, LLC; Linode® è un marchio registrato di Linode, LLC; Vultr® è un marchio registrato di The Constant Company, LLC; Akamai® è un marchio registrato di Akamai Technologies, Inc.; Fastly® è un marchio registrato di Fastly, Inc.; Let’s Encrypt® è un marchio registrato di Internet Security Research Group; Microsoft Corporation detiene i diritti su Microsoft®, Azure®, Windows®, Office®, e Internet Explorer®; Mozilla Foundation detiene i diritti su Firefox®; Apache® è un marchio registrato di The Apache Software Foundation; Apache Tomcat® è un marchio registrato di The Apache Software Foundation; PHP® è un marchio registrato del PHP Group; Docker® è un marchio registrato di Docker, Inc.; Kubernetes® è un marchio registrato di The Linux Foundation; OpenShift® è un marchio registrato di Red Hat, Inc.; Podman® è un marchio registrato di Red Hat, Inc.; Proxmox® è un marchio registrato di Proxmox Server Solutions GmbH; VMware® è un marchio registrato di Broadcom Inc.; CloudFlare® è un marchio registrato di Cloudflare, Inc.; NETSCOUT® è un marchio registrato di NETSCOUT Systems Inc.; ElasticSearch®, LogStash®, e Kibana® sono marchi registrati di Elastic N.V.; Grafana® è un marchio registrato di Grafana Labs; Prometheus® è un marchio registrato di The Linux Foundation; Zabbix® è un marchio registrato di Zabbix LLC; Datadog® è un marchio registrato di Datadog, Inc.; Ceph® è un marchio registrato di Red Hat, Inc.; MinIO® è un marchio registrato di MinIO, Inc.; Mailgun® è un marchio registrato di Mailgun Technologies, Inc.; SendGrid® è un marchio registrato di Twilio Inc.; Postmark® è un marchio registrato di ActiveCampaign, LLC; cPanel®, L.L.C. detiene i diritti su cPanel®; Plesk® è un marchio registrato di Plesk International GmbH; Hetzner® è un marchio registrato di Hetzner Online GmbH; OVHcloud® è un marchio registrato di OVH Groupe SAS; Terraform® è un marchio registrato di HashiCorp, Inc.; Ansible® è un marchio registrato di Red Hat, Inc.; cURL® è un marchio registrato di Daniel Stenberg; Facebook®, Inc. detiene i diritti su Facebook®, Messenger® e Instagram®. Questo sito non è affiliato, sponsorizzato o altrimenti associato a nessuna delle entità sopra menzionate e non rappresenta nessuna di queste entità in alcun modo. Tutti i diritti sui marchi e sui nomi di prodotto menzionati sono di proprietà dei rispettivi detentori di copyright. Ogni altro marchio citato appartiene ai propri registranti. MANAGED SERVER® è un marchio registrato a livello europeo da MANAGED SERVER SRL, con sede legale in Via Flavio Gioia, 6, 62012 Civitanova Marche (MC), Italia e sede operativa in Via Enzo Ferrari, 9, 62012 Civitanova Marche (MC), Italia.

SOLO UN ATTIMO !

Ti sei mai chiesto se il tuo Hosting faccia schifo ?

Scopri subito se il tuo hosting provider ti sta danneggiando con un sito lento degno del 1990 ! Risultato immediato.

Close the CTA
Torna in alto