What Are HTTP/3 and QUIC Proxies?
HTTP/3 and QUIC proxies carry UDP, so a classic HTTP CONNECT proxy cannot relay them. How connect-udp and MASQUE work, and why silent h3 downgrades leak.

HTTP/3 and QUIC proxies are proxies that can forward UDP datagrams rather than only TCP byte streams, which is the one capability a conventional forward proxy does not have.
That single sentence contains the whole problem. HTTP/3 does not run on TCP, so the CONNECT tunnel every commercial proxy is built around has nothing to carry. Most stacks handle this by quietly falling back to HTTP/2 and never telling you. This post covers why the fallback happens, what the IETF actually standardised to fix it, how QUIC's connection IDs and connection migration break a proxy's idea of a "session", and why a client that claims to speak h3 but never does is a detectable pattern.
What Is a QUIC Proxy?
QUIC is a transport protocol defined in RFC 9000, published May 2021. It rides on UDP, folds TLS 1.3 into the transport handshake (RFC 9001), and provides its own streams, flow control, and loss recovery. HTTP/3, defined in RFC 9114 a year later, is the HTTP semantics layer that runs over QUIC. Its ALPN token is the string h3.
So "HTTP/3 proxy" and "QUIC proxy" describe two different jobs that get conflated:
- A proxy that speaks QUIC on the client-facing side, meaning you connect to the proxy itself over HTTP/3. This is easy and increasingly common.
- A proxy that forwards UDP to an arbitrary origin, so your client can complete a real end-to-end QUIC handshake with the target. This is the hard one, and it is what people usually mean when they ask whether a proxy "supports HTTP/3".
The second job needs a datagram-forwarding mechanism that classic proxying never defined. Everything below is downstream of that gap.
Why the Classic Proxy Stack Cannot Carry HTTP/3
An HTTPS request through a forward proxy uses the CONNECT method from RFC 9110 Section 9.3.6. The client asks the proxy to open a TCP connection to host:port, then the proxy relays bytes in both directions without interpreting them. That is the entire contract, and the contract is TCP-shaped. There is no framing in it for a datagram, no way to say "send this 1,200-byte packet and do not retransmit it", no place to put a UDP port that is not also a TCP port. The tunnel mechanics themselves are covered in our piece on what an HTTP tunnel is.
SOCKS5 is the interesting exception, and most write-ups on this topic skip it. RFC 1928 defines a third command alongside CONNECT and BIND: UDP ASSOCIATE, command code 0x03. A SOCKS5 server that implements it opens a UDP relay port, and the client wraps each datagram in a short SOCKS header carrying the destination address. On paper that is enough to carry QUIC.
In practice it almost never works, for reasons worth stating precisely:
- Commercial proxy pools rarely enable UDP ASSOCIATE. Residential and datacenter gateways are built around TCP session accounting, and a UDP relay port breaks both the billing model and the rate limiter.
- Client support is thin. curl does not relay UDP through SOCKS5. Chromium does not send QUIC through a SOCKS5 proxy.
- The relay has to hold per-flow state with no connection close to key it on, so operators expire it aggressively.
| Mechanism | Spec | Carries | Can relay HTTP/3? | Real-world availability |
|---|---|---|---|---|
| HTTP `CONNECT` | RFC 9110 Sec. 9.3.6 | TCP stream | No | Universal |
| SOCKS5 `CONNECT` | RFC 1928 | TCP stream | No | Universal |
| SOCKS5 `UDP ASSOCIATE` | RFC 1928 | UDP datagrams | In theory | Rare on commercial pools, thin client support |
| `connect-udp` | RFC 9298 | UDP datagrams | Yes | Standardised, narrow deployment |
| `connect-ip` | RFC 9484 | IP packets | Yes | VPN-grade, rarer still |
Read that table as the answer to the whole question. Two of the five rows describe what you can actually buy today, and neither one carries QUIC. Our proxy protocol overview covers where each of the classic mechanisms fits.
Scraping at scale? Skip the blocks.
Fast, unblockable datacentre proxies with unlimited bandwidth.
The Silent Downgrade to HTTP/2
Here is the behaviour that catches people out. Nothing errors. Nothing logs a warning. You just get HTTP/2 forever.
A browser learns that an origin supports HTTP/3 in one of two ways: an Alt-Svc response header on an earlier TCP connection, or an HTTPS resource record in DNS (RFC 9460) advertising alpn="h3". Both are visible from the command line:
curl -sI https://cloudflare.com/ | grep -i alt-svc
# alt-svc: h3=":443"; ma=86400
dig +short HTTPS cloudflare.com
# 1 . alpn="h3,h2" ipv4hint=104.16.132.229,104.16.133.229
Now put a proxy in the path. Chromium does not attempt QUIC to an origin when a proxy is configured, because it has no way to get the datagrams there. It uses the proxy's TCP tunnel and negotiates HTTP/2 inside it. The Alt-Svc header still arrives, the browser still parses it, and the QUIC attempt still never happens. The same outcome occurs at the socket layer whenever UDP 443 is filtered anywhere on the path: the handshake times out and the client falls back to TCP.
curl is blunter about it and will tell you outright:
curl --http3-only -x http://user:pass@proxy.sparkproxy.io:8080 https://example.com/
# curl: (5) HTTP/3 cannot be used with a proxy
With no proxy in the path, and a curl built against a QUIC-capable TLS stack such as ngtcp2 with quictls or quiche, the positive case works. curl shipped HTTP/3 experimentally from 7.66 in 2019 and dropped that label during the 8.x series; the official build notes list the current backends:
curl --http3-only -w '%{http_version}\n' -o /dev/null -s https://cloudflare-quic.com/
# 3
The practical takeaway: if any element in your chain is a classic proxy, your traffic is HTTP/2 over TCP no matter what your client library claims to support. Treat that as a given, then decide whether it matters.
RFC 9298: connect-udp and MASQUE
The standards-track answer exists and is finished. RFC 9298, "Proxying UDP in HTTP", published August 2022, defines a connect-udp upgrade token used with extended CONNECT. The client issues a request whose target identifies the destination host and port through a URI template, and on a 200 response the tunnel carries UDP payloads as HTTP Datagrams.
:method = CONNECT
:protocol = connect-udp
:scheme = https
:path = /.well-known/masque/udp/192.0.2.6/443/
:authority = proxy.sparkproxy.io
capsule-protocol = ?1
Three companion specs do the plumbing:
- RFC 9297 defines HTTP Datagrams and the Capsule Protocol, the generic way to move datagram-shaped data inside an HTTP request stream.
- RFC 9221 adds an unreliable DATAGRAM frame to QUIC itself, so when the proxy connection is HTTP/3 the inner UDP packets are not retransmitted by the outer transport.
- RFC 9484 generalises the idea to whole IP packets with
connect-ip, which is closer to a VPN than to a proxy.
That combination matters more than it looks. Running connect-udp over an HTTP/2 proxy connection works, but the inner QUIC stream inherits TCP's head-of-line blocking and retransmission from the outer connection, so you get QUIC semantics with TCP behaviour. Running it over HTTP/3 with RFC 9221 datagrams preserves the loss characteristics QUIC was designed around. If somebody offers you a "QUIC proxy" that runs over HTTP/2, that distinction is the question to ask.
The umbrella name for this family is MASQUE, the IETF working group whose charter and document set cover all of the above.
Adoption Reality in 2026
Being honest about deployment is more useful than being enthusiastic about it. MASQUE runs at very large scale, and almost none of it is available to you as a proxy you can buy per gigabyte.
Where it is deployed:
- Apple iCloud Private Relay proxies traffic across a two-hop relay architecture built on this family of specs.
- Google's IP Protection in Chrome, described in the Privacy Sandbox documentation, proxies requests to a list of third-party domains.
- Cloudflare WARP moved its tunnel to MASQUE, documented on the Cloudflare blog.
In all three, the client and the proxy are built by the same vendor and versioned together, which is a very different product from an open endpoint that arbitrary software can point at.
Where the tooling is:
- masque-go from the quic-go project implements connect-udp for both roles.
- Envoy exposes CONNECT-UDP as an HTTP upgrade type.
- aioquic gives Python a QUIC and HTTP/3 implementation, useful for testing how your own endpoints behave.
What is missing is the demand side. No mainstream HTTP client library exposes connect-udp as a one-line proxy setting the way HTTPS_PROXY exposes CONNECT. Until a client can be pointed at a MASQUE proxy without custom code, providers have little reason to sell one, and the loop stays closed. Expect that to break first in privacy tooling, not in scraping infrastructure.
Connection IDs and What a "Session" Means
This is where QUIC stops being "TCP over UDP" and starts breaking assumptions that proxy software is built on.
TCP identifies a connection by the 4-tuple of source IP, source port, destination IP, destination port. Change any element and it is a different connection. Every NAT, load balancer, firewall state table, and proxy session tracker in production encodes that assumption somewhere.
QUIC identifies a connection by a connection ID, defined in RFC 9000 Section 5.1. Each endpoint chooses connection IDs that the peer must place in the destination field of outgoing packets. The IDs are opaque, variable length up to 20 bytes, and each side can hand the peer spares with NEW_CONNECTION_ID frames and retire old ones with RETIRE_CONNECTION_ID. A client that does not need packets routed back to a specific instance may use a zero-length connection ID and rely on the 4-tuple instead, which is what browsers commonly do.
Three consequences follow directly:
- Server-side load balancers route on the connection ID, not the IP. A server fleet can encode routing information into the ID it issues, so packets reach the right instance regardless of the client's address.
- Sticky sessions stop being an IP property. A sticky session proxy holds one exit IP for a window because that is how a server correlates your requests. Under QUIC the connection ID does the correlating, and it survives your IP changing.
- Connection IDs rotate deliberately for unlinkability. RFC 9000 requires an endpoint that moves to a new path to use a previously unused connection ID, so an on-path observer cannot trivially stitch the old address to the new one. The two endpoints still know it is one connection.
Points 2 and 3 pull in opposite directions, and which one dominates depends entirely on who is observing. Against a passive network observer, connection IDs help you. Against the origin server you are talking to, they do the opposite of what IP rotation is meant to achieve.
Connection Migration Breaks IP-Pinned Sessions
RFC 9000 Section 9 specifies connection migration. A client can move to a new IP address or port and continue the same QUIC connection, keeping its congestion state, its open streams, and its keys. The server validates the new path with a PATH_CHALLENGE frame containing random data and expects the matching PATH_RESPONSE. Only clients may initiate migration. Servers may not.
Two details are load-bearing for anyone routing this through infrastructure:
- Until a new path is validated, the server limits what it sends there to three times what it received, the anti-amplification rule in RFC 9000 Section 8. Path changes therefore carry a throughput dip, not just a latency blip.
- The same machinery absorbs NAT rebinding. When a carrier NAT reassigns your source port mid-connection, TCP breaks and QUIC survives.
For proxy design this inverts a familiar assumption. With TCP, rotating the exit IP forces a new connection, which most people treat as a clean slate. With QUIC, a mid-connection address change is a supported operation that the server explicitly ties back to the connection it already has. The IP moved, the identity did not. If your rotation strategy assumes new IP equals new session, QUIC quietly invalidates it. Classic proxy rotation still behaves as expected on TCP paths.
0-RTT Resumption and the Replay Caveat
QUIC inherits TLS 1.3 session resumption, and RFC 9001 Section 4.6 describes 0-RTT: a client holding a valid ticket from a previous connection can send application data in its very first flight, before the handshake completes. On a warm connection to a nearby edge, that removes a full round trip.
The caveat is not subtle, and the specs state it plainly. Early data is replayable. An attacker who captures the first flight can send it again, and the server has no handshake context to reject it with. RFC 8470 covers using early data in HTTP and defines the 425 Too Early status for servers that would rather not guess. RFC 9114 restricts what HTTP/3 may put in early data for the same reason.
Practical rules if you build on this:
- Only idempotent requests belong in early data. A GET for a product page is fine. Anything that mutates state is not.
- Handle
425 Too Earlyby retrying on the completed handshake. Treating it as a plain failure produces flakiness you will struggle to reproduce. - Anti-replay is the server's job and implementations vary: once per ticket, a bounded replay window, or switched off entirely.
There is a second-order effect worth naming. Accepting your 0-RTT means the server matched your resumption ticket to a prior session. That is deliberate cross-connection linkage: you have just told the origin that these two connections are the same client, from a different IP, with no cookie involved. If unlinkability is the point of your proxy setup, resumption tickets are a leak you have to manage, in the same family as TLS fingerprinting.
Encrypted Transport Headers and Middlebox Blindness
QUIC encrypts nearly everything TCP leaves in the clear. RFC 8999 fixes the small set of version-independent fields that stay readable forever, and RFC 9001 Section 5.4 adds header protection that masks the packet number and key phase bits. Acknowledgements, retransmissions, stream boundaries, and flow control all sit inside the encrypted envelope.
| Observable | TCP + TLS 1.3 | QUIC |
|---|---|---|
| Destination IP and port | Visible | Visible |
| SNI / server name | Visible in ClientHello | Visible in the Initial packet |
| ALPN | Visible in ClientHello | Visible in the Initial packet |
| Sequence / packet numbers | Visible | Encrypted (header protection) |
| ACKs and retransmissions | Visible | Encrypted |
| Connection start and end | SYN and FIN visible | Handshake visible, close encrypted |
| Round-trip time | Derivable from ACKs | Only via the optional spin bit |
The SNI row is the one people get wrong. QUIC Initial packets are protected with keys derived from the client's Destination Connection ID, which is sitting right there on the wire. Any on-path observer that implements the derivation can decrypt Initial packets and read the TLS ClientHello, including SNI and ALPN. QUIC hides the transport, not the handshake. Encrypted ClientHello is a separate mechanism with a separate deployment story.
Everything after the handshake is genuinely opaque. That is why RFC 9000 Section 17.3.1 defines the latency spin bit, an optional single-bit signal that lets a passive observer estimate round-trip time. Without it, operators lose every tool they use to debug a network.
For scraping, the useful inversion is this: detection moves from the middle of the network to the endpoint. A transit provider sees less than it did under TCP. The origin server sees more, because QUIC handshake parameters are a richer fingerprinting surface than a TLS ClientHello on its own. The JA4+ suite makes that concrete. A JA4 fingerprint begins with q when the connection is QUIC and t when it is TCP, so the transport you used is literally the first character of your fingerprint.
The Scraping Signal: Advertising h3 and Never Speaking It
Now put the pieces together, because this is the consequence that matters.
Your headless browser presents itself as Chrome. Its TLS ClientHello matches Chrome. Its HTTP/2 SETTINGS frame and header order match Chrome. Its User-Agent says Chrome. And it has never, across a hundred thousand requests, completed a single HTTP/3 handshake, because it sits behind a CONNECT proxy that cannot carry UDP.
Real Chrome on a real residential connection behaves nothing like that. It queries DNS for HTTPS records, it honours Alt-Svc, and on any Cloudflare-fronted, Google-fronted, or Fastly-fronted origin it moves to h3 quickly and stays there for the advertised ma lifetime. UDP 443 is open on the overwhelming majority of consumer networks.
So the origin can log a straightforward inconsistency:
| Observation | Real Chrome on residential | Chrome behind a CONNECT proxy |
|---|---|---|
| JA4 first character | Mostly `q` after the first visit | Always `t` |
| Follows `Alt-Svc: h3` on the next request | Yes | Never |
| Queries HTTPS RR type 65 | Yes | Resolver-dependent, often no |
| Ratio of h3 to h2 requests per client | High | Exactly zero |
| Claimed client identity | Chrome | Chrome |
No single row there is a block on its own. Stacked into a scoring model alongside the rest of the headless browser detection surface, "claims to be a browser that normally uses h3, has never once used h3" is a cheap and stable feature. It costs the defender nothing to compute, because the data already sits in their edge logs.
Keep the risk in proportion, though. Plenty of legitimate traffic never speaks h3: corporate networks that block UDP 443, older clients, restricted mobile networks. The signal is weak in isolation and strong in combination, which is exactly how modern bot scoring works.
The practical way out is to stop pretending. Either present as a client that plausibly cannot do h3, keeping your User-Agent and TLS fingerprint consistent with that story, or move the HTTP/3 handshake somewhere that can actually complete it. A scraping API terminates the request on its own infrastructure, so the transport your code uses locally is decoupled from the transport that reaches the origin:
curl -X GET "https://scrape.sparkproxy.io/api/v1?url=https://example.com&render_js=true&stealth=true" \
-H "X-API-Key: YOUR_API_KEY"
The same request as a JSON POST, with a country pin and a real browser render:
import requests
r = requests.post(
"https://scrape.sparkproxy.io/api/v1",
headers={
"X-API-Key": "YOUR_API_KEY",
"Content-Type": "application/json",
},
json={
"url": "https://example.com/pricing",
"render_js": True,
"stealth": True,
"premium_proxy": True,
"country_code": "de",
"format": "html",
},
timeout=120,
)
print(r.status_code, r.headers.get("X-Credits-Used"))
The full parameter reference lives in the SparkProxy Scraping API docs. Note the response headers: X-Job-Id, X-Credits-Used, and X-Duration-Ms come back on plain HTTP responses, which is enough to reconcile spend without turning on the JSON envelope.
What to Actually Do Today
A short, unromantic checklist:
- Assume your proxied traffic is HTTP/2 over TCP. Verify rather than assume, with
curl -o /dev/null -s -w '%{http_version}\n'through your actual proxy chain. - Do not advertise capabilities you cannot deliver. If your stack cannot complete h3, do not fingerprint as a client that always does.
- Ask providers a specific question. Not "do you support HTTP/3" but "do you implement RFC 9298 connect-udp, or SOCKS5 UDP ASSOCIATE?" The vague version gets a vague yes, because the proxy's own front end speaks HTTP/3 and that answer is technically true.
- Stop treating IP rotation as a session reset on QUIC paths. Connection IDs and migration let the origin follow you across addresses.
- Keep early data idempotent and handle
425 Too Earlyinstead of counting it as a failure. - Watch the MASQUE working group, not vendor blog posts, if you want to know when general-purpose QUIC proxying becomes something you can buy.
None of it is urgent. All of it is cheap now and expensive to retrofit once h3 is the default path for most of the web.
Frequently asked questions
FAQ
No. The CONNECT method defined in RFC 9110 opens a TCP tunnel, and HTTP/3 runs on QUIC over UDP, so there is nothing for that tunnel to carry. Clients silently fall back to HTTP/2 over TCP instead, and curl refuses outright with "HTTP/3 cannot be used with a proxy".
Not for fetching content, since every major site still serves the same responses over HTTP/2. They matter for consistency: a client that fingerprints as Chrome but never completes an h3 handshake is a detectable mismatch, and it costs a defender almost nothing to log.
MASQUE is the IETF working group; connect-udp is one of the protocols it produced, specified in RFC 9298. The family also includes HTTP Datagrams and the Capsule Protocol in RFC 9297, QUIC unreliable datagrams in RFC 9221, and connect-ip for full IP packet forwarding in RFC 9484.
Yes, from the server's point of view. RFC 9000 Section 5.1 makes the connection ID the identifier for a QUIC connection, so a server correlates your requests by that ID rather than by your 4-tuple, and changing exit IP mid-connection does not hand you a fresh identity.
Only for idempotent requests. Early data is replayable by design, which is why RFC 8470 defines the 425 Too Early status, and accepting your resumption ticket also tells the origin that two connections from different IPs belong to the same client.
No, it forces a fallback rather than a failure. Clients that cannot complete the QUIC handshake retry over TCP, which is the same downgrade a CONNECT proxy already causes, so the cost is a slower first connection and a missing h3 signal rather than a broken request.
Get 20% off your first month
Premium datacentre proxies with unlimited bandwidth. Use the code at checkout.
Save up to 15% more on quarterly, half-yearly and yearly plans
Related articles

What Are Zero-Log Proxies and Why Logging Policy Matters
Zero-log proxies explained: what an operator must record to run a network at all, how to read a logging policy, and four tests that check the claim yourself.

What Are Sneaker Proxies and How They Work
Sneaker proxies explained: what makes an IP survive a limited release, why /24 subnet concentration gets whole pools banned, and where the legal line sits.

What Are Gaming Proxies? An Honest Latency Breakdown
Gaming proxies explained honestly: why an extra hop raises ping and jitter, why UDP traffic needs SOCKS5, and the five jobs a proxy actually does well.
