HTTP/3 does not go through your proxy
Enable HTTP/3 in a client, point it at a proxy, and you get one of three outcomes: an error you have never seen, a request that succeeds while the proxy shows no traffic for it, or everything working normally with HTTP/2 quietly substituted. All three come from the same gap. None is a fault in the proxy pool, and buying UDP support closes none of them in a browser.
The gap is simple to state. HTTP/3 is not HTTP over TCP: it runs over QUIC, QUIC runs over UDP, and proxying, as essentially everyone deploys it, is TCP.
The mechanism, once
An HTTP proxy gives a client exactly one primitive for encrypted traffic: CONNECT. The client opens a TCP connection to the proxy, sends CONNECT example.com:443, the proxy opens its own TCP connection to the target, replies 200, and from then on copies bytes in both directions. That is the entire contract: a TCP byte pipe, with no frame in it that can carry a UDP datagram.
QUIC does not want a byte pipe. It wants discrete datagrams sent to port 443/UDP on the target, each independently routable, each with its own loss detection, and it wants to survive the client’s address changing underneath it. CONNECT has nothing to translate that into.
So an HTTP/3 request through an HTTP proxy either does not happen, or happens without the proxy.
Which of the three am I looking at
It fails outright. What you get from a client explicitly told to use HTTP/3 and a proxy at once. Careful clients refuse the combination up front and document it — curl’s own manual states that “curl cannot do HTTP/3 over any proxy”. Less careful ones start a QUIC handshake and sit there until it times out. Recent curl builds do add --proxy-http3, but that is HTTP/3 on the hop to an HTTPS proxy which supports it, not HTTP/3 carried through to the target.
It falls back to TCP. What browsers do, and the benign outcome. With a proxy configured, Chrome and Firefox route the request through it over TCP and do not attempt QUIC to the origin, whatever the site advertises. You get h2, or h1.1 if the target does not offer h2. Nothing is broken; your HTTP/3 support is simply not in play.
It bypasses the proxy entirely. The one that costs you. It happens whenever the proxy sits at a layer that only sees TCP: a system-wide setting an application honours for HTTP but not for its own UDP sockets, a firewall rule redirecting outbound TCP 80 and 443, a transparent proxy in front of a container. TCP goes through the proxy; UDP 443 walks out of the host on its own address. Requests succeed, logs look healthy, and the exit IP is yours.
The third case is why this article exists. A proxy failure is loud; a proxy bypass is silent and looks like success.
Two commands that tell you which
The first asks what was negotiated and who you actually connected to.
curl -sS -o /dev/null \
-x "http://USER:PASS@PROXY_HOST:PORT" \
-w 'http=%{http_version} peer=%{remote_ip}\n' \
https://example.com
http_version is the HTTP version that was effectively used. remote_ip is the remote address of the connection curl most recently made — with a proxy in play, that is the proxy. If peer comes back as the target’s own address, that request did not go through the proxy, whatever your configuration says.
The second compares exits directly.
curl -sS -x "http://USER:PASS@PROXY_HOST:PORT" https://api.ipify.org; echo
curl -sS https://api.ipify.org; echo
If both print the same address, nothing is being proxied, and you found it before opening a ticket with anyone.
In a browser, open DevTools, go to the Network panel, right-click the column headers and enable Protocol. Every request then shows h3, h2 or http/1.1. With a proxy configured you should see no h3 at all; if you do, that request left the machine on its own.
What SOCKS5 UDP ASSOCIATE actually is
SOCKS5 does have a UDP mechanism, and the gap between what it does and what people assume it does is the whole confusion.
The client opens the usual TCP control connection and issues UDP ASSOCIATE instead of CONNECT. The server replies with the address and port of a UDP relay. The client then sends datagrams to that relay — but not raw. Each one must be prefixed with a short SOCKS header giving the destination address and port, and each datagram coming back arrives with the same header for the client to strip. The association lives only as long as the TCP control connection; drop that and the relay stops accepting your packets. All of this is RFC 1928.
Three consequences follow.
A UDP stack cannot use a SOCKS relay by accident. The application’s datagram layer has to be SOCKS-aware, because it adds and strips that header on every packet. Pointing an existing QUIC implementation at the relay does not work: the target would receive QUIC packets with a SOCKS header glued to the front — ten bytes for an IPv4 destination, more for a hostname or an IPv6 address — and drop them.
Overhead lands awkwardly. QUIC requires the datagrams carrying its Initial packets to be at least 1,200 bytes, and the SOCKS header goes on top. It fits a normal 1,500-byte path but eats headroom, and further encapsulation starts producing fragmentation and silent drops.
And most providers do not implement it at all. Many endpoints advertised as SOCKS5 support CONNECT and nothing else, because TCP is what customers use — so “SOCKS5” on a pricing page tells you nothing about UDP.
Why UDP support still does not get QUIC into your browser
Here is the part the marketing pages leave out. Even against a proxy that implements UDP ASSOCIATE correctly, mainstream browsers will not send QUIC through it.
Chromium’s SOCKS client is a TCP client. It does not relay datagrams, so Chrome’s QUIC stack has no path to a SOCKS relay even when one is waiting. Firefox behaves the same way, and Mozilla has it on file as a known limitation. Neither exposes a setting to change it: the work is teaching a QUIC implementation to speak SOCKS at the datagram layer, then handling what a relay does to path validation and connection migration.
So: a provider supporting UDP ASSOCIATE is a statement about the proxy. A browser using it is a statement about the client. The second does not follow from the first, and the second is the one you needed.
The standards-track answer is CONNECT-UDP, defined in RFC 9298 and used by MASQUE deployments. It is the right shape — datagrams carried inside an HTTP connection rather than pretending to be a stream — but it needs both ends, and no mainstream browser lets you point its proxy settings at one.
We ship UDP ASSOCIATE on exactly one product, our IPv6 plan, and it will not make Chrome speak QUIC. For nearly every scraping and automation workload the right move is to turn HTTP/3 off in the client, not to buy UDP support.
What you lose by falling back to HTTP/2
Less than people fear, and the reason is structural. A browser does not know a site speaks h3 until it is told: usually by an Alt-Svc header on a connection it already has, and more recently by an HTTPS DNS record carrying an h3 ALPN value. Either way, every origin offering h3 also serves h2 or h1.1 over TCP, because it has to. A session that stays on h2 is doing what every client behind a network blocking UDP 443 does permanently.
What you give up is performance on lossy links: QUIC avoids head-of-line blocking across streams, resumes faster, and survives a network change without rebuilding the connection. Fetching pages one at a time over a stable connection, that difference is small, and dwarfed by the proxy hop you added on purpose.
What targets react to is inconsistency, not version. A client presenting itself as one browser while negotiating a TLS handshake belonging to another is a mismatch. Using h2 is not.
Where UDP support genuinely earns its keep
Not in a browser, and not in scraping. It earns its keep when you control both ends of a datagram protocol and your client speaks SOCKS: DNS straight over UDP, custom QUIC clients built with a SOCKS-aware datagram layer, protocols with no TCP mode at all. For those, “do you support UDP ASSOCIATE” is exactly the right question.
Making the behaviour predictable
Turn HTTP/3 off at the client rather than hoping the fallback path is clean.
- Chrome, Edge and other Chromium builds: launch with
--disable-quic, orchrome://flags→ Experimental QUIC protocol → Disabled. In a managed environment, theQuicAllowedpolicy set to false does it centrally and stops a user turning the flag back on. - Playwright, Puppeteer and Selenium: pass
--disable-quicin the launch arguments. This is the one people forget, because a headless browser inherits nothing from the desktop profile. - Firefox:
about:config→ setnetwork.http.http3.enabletofalse. - curl: HTTP/3 is opt-in via
--http3, so the default is safe. Pin it with--http2or--http1.1if a wrapper might add flags for you. - curl_cffi and other impersonation libraries: set the HTTP version explicitly rather than inheriting the profile’s default.
- requests, httpx, undici, Go’s net/http: none speak HTTP/3 without an extra library, so there is nothing to disable.
Then close the door at the host, which catches anything you missed. On Linux use REJECT, not DROP — the next section explains why:
iptables -A OUTPUT -p udp --dport 443 -j REJECT
New-NetFirewallRule -DisplayName "Block QUIC" -Direction Outbound -Protocol UDP -RemotePort 443 -Action Block
The failure that shows up as a timeout
A rejected packet produces an ICMP unreachable, so the client learns at once that UDP 443 is closed and falls back to TCP straight away. A dropped packet produces nothing: the client waits out its QUIC handshake timeout, retries, waits again, and only then falls back — if it falls back at all. One configured to require HTTP/3 never does.
That is why this class of problem arrives as “the proxy is slow” or “requests hang, then work”. No 407, no 502, no connection refused — just a client waiting for an answer to a datagram nothing on the path will acknowledge. When you see a fixed, repeatable delay in front of otherwise healthy requests, check whether something is attempting QUIC before you look at the proxy.
What to ask a provider
Not “do you support UDP” — ask what your own client does, then whether the provider’s answer changes it. Driving a browser it changes nothing, and any page implying otherwise describes a capability with no route from your client to it: disable HTTP/3, confirm on the Protocol column that you are on h2, and move on. Writing your own datagram client, it is worth paying for — and worth testing by sending one datagram end to end before you commit.
The transport gap is real, and no proxy vendor is going to close it. It closes when browsers can be pointed at a CONNECT-UDP proxy, and that is not today.