๐ŸŽ‰ Premium Proxies ยท 3-Day Free TrialClaim Now
Proxy Basic

TCP vs UDP Proxies: What the Transport Layer Decides

TCP vs UDP proxies explained: HTTP proxies are TCP-only, SOCKS5 UDP ASSOCIATE is rare, and DNS, QUIC, HTTP/3 and WebRTC all change once UDP is gone.

S SparkProxy 2 21 min read
Share
TCP vs UDP Proxies: What the Transport Layer Decides

TCP vs UDP proxies comes down to a single capability: HTTP and HTTPS proxies relay TCP and nothing else, so UDP only crosses a proxy through SOCKS5 UDP ASSOCIATE (command 0x03) or the newer HTTP connect-udp extension, and without one of those, DNS, QUIC, HTTP/3 and WebRTC either escape around the proxy or silently downgrade to TCP.

Most proxy comparisons stop at the application layer: HTTP versus SOCKS5, residential versus datacenter, rotating versus sticky. The boundary that quietly breaks more setups sits one layer down, and it is not a preference you tune, it is a hard capability an implementation either has or does not. This guide covers what actually stops working when UDP is missing, why the failures are usually silent instead of loud, and how to test a proxy for real UDP support in under a minute.

TCP vs UDP at the Transport Layer

TCP (RFC 9293, which consolidated the original RFC 793 in August 2022) is a connection-oriented byte stream. It handshakes before sending, numbers every byte, retransmits what gets lost, and hands the application data in the exact order it was sent. UDP (RFC 768, 1980) is a datagram service with an 8-byte header: source port, destination port, length, checksum. That is the whole feature set. No handshake, no ordering, no retransmission, no congestion control unless the application builds its own.

PropertyTCPUDP
Connection setup3-way handshake, 1 RTT before first bytenone, first datagram carries data
Header size20 bytes minimum, 60 with options8 bytes fixed
Deliveryreliable, retransmits lost segmentsbest effort, losses are the app's problem
Orderingstrict, in-order byte streamnone, datagrams can arrive out of order
Congestion controlbuilt in (CUBIC, BBR)must be implemented by the application
Multiplexingone stream per connectionone socket serves many peers
Typical usersHTTP/1.1, HTTP/2, TLS, SSH, SMTP, IMAPDNS, QUIC and HTTP/3, WebRTC media, STUN, NTP, game netcode, syslog

The consequence for proxies is direct. TCP is a connection a middlebox can accept, terminate, and re-originate. UDP is a stream of independent packets with no session state to hold on to. Relaying UDP means inventing a mapping table, a timeout policy, and a return path. That is real engineering work, and it is why so much proxy infrastructure simply skips it.

Why HTTP Proxies Are TCP-Only by Construction

An HTTP proxy has exactly two modes, and both assume TCP.

Absolute-form requests. For plaintext HTTP, the client sends the full URL in the request line and the proxy makes its own TCP connection to the origin:

GET http://example.sparkproxy.io/pricing HTTP/1.1
Host: example.sparkproxy.io
Proxy-Authorization: Basic dXNlcjpwYXNz

The CONNECT tunnel. For HTTPS, the client asks the proxy to open a raw pipe, defined in RFC 9110 section 9.3.6:

curl -x http://user:pass@proxy.sparkproxy.io:10000 \
     -v https://example.sparkproxy.io/api/status
# > CONNECT example.sparkproxy.io:443 HTTP/1.1
# < HTTP/1.1 200 Connection established

Read that CONNECT request line again. It carries a host and a port and nothing else. There is no field that says "and by the way, make this a UDP association." HTTP never defined one, because HTTP itself has always run over a reliable stream. CONNECT establishes a tunnel over the existing TCP connection, and the proxy's outbound socket is SOCK_STREAM by definition. If you want the byte-level walkthrough of that handshake, our breakdown of proxy protocols and how HTTP, HTTPS and SOCKS5 differ covers it.

So when someone sells you an "HTTPS proxy," they are selling a TCP relay. Every UDP-based protocol on your machine then either finds another route (a leak) or silently degrades to a TCP equivalent (a fingerprint anomaly). Neither one shows up as an error.

Free trial

Scraping at scale? Skip the blocks.

Fast, unblockable datacentre proxies with unlimited bandwidth.

SOCKS5 UDP ASSOCIATE: The Only Common Escape Hatch

SOCKS5, specified in RFC 1928 (March 1996), defines three commands in the request byte CMD:

CMD byteCommandTransport
`0x01`CONNECToutbound TCP
`0x02`BINDinbound TCP, for protocols like active FTP
`0x03`UDP ASSOCIATEUDP relay

UDP ASSOCIATE is the only widely deployed way to push UDP through a proxy, and its mechanics catch people out. You do not send UDP to the proxy's SOCKS port. Instead:

  1. Open a normal TCP connection to the SOCKS5 port (commonly 1080) and authenticate.
  2. Send UDP ASSOCIATE with the address and port you intend to send datagrams from. 0.0.0.0:0 is legal and common when you do not know yet.
  3. The server replies with BND.ADDR and BND.PORT. That is the relay socket you send datagrams to.
  4. Prefix every datagram you send with a SOCKS UDP request header naming the real destination.
  5. Keep the TCP control connection open. When it closes, the association is torn down. This is the single most common bug in home-grown SOCKS clients.

The per-datagram header is small and fixed for IPv4:

OffsetFieldSizeValue
0`RSV`2 bytes`0x0000`
2`FRAG`1 bytefragment number, `0x00` for standalone
3`ATYP`1 byte`0x01` IPv4, `0x03` domain, `0x04` IPv6
4`DST.ADDR`4 / 1+n / 16 bytesdestination address
variable`DST.PORT`2 bytesbig-endian port
variable`DATA`rest of packetyour payload

An IPv4 datagram therefore carries 10 bytes of overhead. That matters near the path MTU, because the SOCKS header pushes you closer to fragmentation. The FRAG field exists for splitting oversized datagrams, and in practice almost nobody implements it: most servers accept FRAG = 0x00 and drop anything else. RFC 1928 section 7 explicitly permits a server to decline fragmentation.

Two things break in the field. First, many providers answer UDP ASSOCIATE with reply code 0x07, "command not supported," which is at least an honest failure. Second, and worse, some accept the association, hand you a BND.ADDR on a private range like 10.x.x.x, and then nothing ever comes back, because the relay sits behind NAT the return path cannot cross. A successful reply byte is not proof of a working relay. You have to send a datagram and read an answer.

If you are still deciding between protocol versions, note that SOCKS4 has no UDP command at all. Our SOCKS4 vs SOCKS5 comparison and the SOCKS5 vs HTTP proxy breakdown cover the remaining differences, and the SOCKS proxy primer walks the handshake bytes.

DNS Is UDP by Default

Name resolution is the first thing that touches UDP in almost every request you make. A standard query goes to port 53 over UDP. TCP is the fallback, used when a response exceeds what the transport will carry and the server sets the TC (truncated) flag, or when the resolver simply prefers it. RFC 7766 (March 2016) made DNS-over-TCP support mandatory for implementations, but it did not make TCP the default path.

This is why the socks5 versus socks5h distinction exists in curl and in Python's requests:

# Resolves the hostname LOCALLY, over UDP, outside the tunnel. Leaks.
curl --socks5 user:pass@proxy.sparkproxy.io:1080 https://example.sparkproxy.io/

# Sends the hostname to the proxy, which resolves it. No local DNS.
curl --socks5-hostname user:pass@proxy.sparkproxy.io:1080 https://example.sparkproxy.io/
# requests / PySocks: the trailing "h" is the whole difference
proxies_leaky = {"https": "socks5://user:pass@proxy.sparkproxy.io:1080"}
proxies_safe  = {"https": "socks5h://user:pass@proxy.sparkproxy.io:1080"}

Here is the subtle part. Hostname-at-the-proxy resolution works fine on a TCP-only SOCKS5 server, because the proxy does the UDP query on its own network. Your machine sends nothing to port 53. That is a genuine fix for the leak, and it is why a TCP-only proxy is still safe for ordinary scraping as long as you use socks5h or let an HTTP proxy resolve the hostname from CONNECT.

Where it stops working is when your code needs to speak DNS directly: a custom resolver, a DNS enumeration tool, dig pointed at a specific nameserver, or any check that must originate from the exit IP. Those need real UDP, or an explicit TCP fallback:

# Force DNS over TCP so it can traverse a TCP-only tunnel
dig +tcp @1.1.1.1 example.sparkproxy.io A

# The default: UDP, which a TCP-only proxy cannot carry at all
dig +notcp @1.1.1.1 example.sparkproxy.io A

For the full procedure, see our proxy DNS leak testing guide.

QUIC and HTTP/3 Ride UDP, So a TCP-Only Proxy Forces a Downgrade

QUIC (RFC 9000, May 2021) runs over UDP, usually port 443. HTTP/3 (RFC 9114, June 2022) is HTTP mapped onto QUIC. A browser learns that HTTP/3 is available in one of two ways: an Alt-Svc: h3=":443" response header on an earlier HTTP/1.1 or HTTP/2 response, or an HTTPS resource record in DNS (RFC 9460, November 2023) carrying alpn="h3".

Now put a TCP-only proxy in the path. The QUIC attempt either never happens, because the browser knows the proxy cannot carry it, or it is sent and dies with no reply. Either way the client falls back to HTTP/2 over TCP and the page loads. Nothing errors. Nothing logs. You will not notice.

That silent fallback deserves more attention than it gets, because it is observable from the server side. A site that advertises h3 sees a large, measurable share of current Chrome and Firefox traffic arrive over HTTP/3; Cloudflare Radar publishes the live HTTP version split if you want the number for the week you are reading this. A client presenting a modern Chrome User-Agent and a matching TLS fingerprint that never once negotiates HTTP/3, across thousands of requests, is a real inconsistency in the profile. It is not by itself a block trigger, and plenty of legitimate corporate networks block UDP 443 outright, which is exactly why it is a weak signal rather than a strong one. It still adds to a pile that already includes header order, TLS ClientHello shape, and timing.

You can watch the fallback happen:

# Direct: negotiates HTTP/3 where the origin supports it
curl --http3-only -sI https://cloudflare-quic.com/ -o /dev/null -w '%{http_version}\n'
# 3

# Through a TCP-only proxy: the QUIC attempt cannot leave
curl --http3-only -x http://user:pass@proxy.sparkproxy.io:10000 \
     -sI https://cloudflare-quic.com/ -o /dev/null -w '%{http_version}\n'
# curl: (97) cannot connect through proxy with HTTP/3

For scraping this is usually a non-issue. Server-side rendering over HTTP/2 is fine for the vast majority of targets, and every major anti-bot vendor still serves HTTP/2 happily. When a target genuinely behaves differently over HTTP/3, the practical answer is to move the request to a managed rendering layer instead of trying to tunnel QUIC yourself. The SparkProxy Scraping API runs a real headless Chromium on our side of the connection, so transport negotiation happens between our browser and the origin, not across your proxy tunnel:

curl -G "https://scrape.sparkproxy.io/api/v1" \
  -H "X-API-Key: $SPARKPROXY_API_KEY" \
  --data-urlencode "url=https://example.sparkproxy.io/catalog" \
  --data-urlencode "render_js=true" \
  --data-urlencode "premium_proxy=true" \
  --data-urlencode "country_code=DE" \
  --data-urlencode "format=json"
import os, requests

resp = requests.get(
    "https://scrape.sparkproxy.io/api/v1",
    headers={"X-API-Key": os.environ["SPARKPROXY_API_KEY"]},
    params={
        "url": "https://example.sparkproxy.io/catalog",
        "render_js": "true",
        "stealth": "true",
        "wait_for": "#product-grid",
        "format": "json",
    },
    timeout=120,
)
data = resp.json()
print(data["status_code"], data["duration_ms"], data["credits_used"])

WebRTC: The Classic UDP Leak

WebRTC is the textbook case of UDP escaping a proxy, and it predates QUIC as a leak vector by years.

When a page calls RTCPeerConnection, the browser runs ICE (RFC 8445) to gather candidate addresses. Host candidates come from local interfaces. Server-reflexive candidates come from a STUN binding request (RFC 8489) sent over UDP, typically to port 3478, which returns the public IP the packet arrived from. Those UDP sockets are opened by the browser's media stack, not by its HTTP stack, and a proxy configured for HTTP traffic does not sit on that path. The STUN response therefore reports your real ISP address while every page load reports the proxy's exit IP.

Browsers expose a policy switch for exactly this, and the behaviour it selects is specified in RFC 8828, "WebRTC IP Address Handling Requirements":

# Chrome / Chromium: refuse any WebRTC UDP that is not proxied
chrome --proxy-server="socks5://proxy.sparkproxy.io:1080" \
       --force-webrtc-ip-handling-policy=disable_non_proxied_udp
# Firefox about:config
media.peerconnection.ice.proxy_only = true

Set that and the leak stops. What replaces it depends entirely on the proxy's transport. If the proxy does real UDP ASSOCIATE, WebRTC relays media through it. If it is TCP-only, ICE falls back to TURN over TCP where a TURN server is configured, and where one is not, the connection never establishes. A video call that hangs at "connecting" is the expected outcome of a TCP-only proxy plus disable_non_proxied_udp, and it is the correct trade: no media beats a leaked IP. Our WebRTC leaks explainer covers detection and the antidetect-browser angle in more depth.

For headless scraping the cleanest answer is different: disable WebRTC entirely. A scraper has no reason to carry a media stack.

Head-of-Line Blocking and What TCP Ordering Costs

TCP's in-order guarantee is not free, and the bill arrives as latency under packet loss.

Picture segments 1 through 10 in flight. Segment 4 is dropped. Segments 5 through 10 arrive and sit in the receiver's buffer, complete and intact, but the kernel will not hand any of them to the application, because doing so would break the byte-stream contract. Everything waits for the retransmission of segment 4, which costs at least one round trip and often more when loss detection needs a retransmission timeout rather than duplicate ACKs. That is transport-layer head-of-line blocking.

HTTP/2 made it worse in one specific way. It multiplexes many logical streams over a single TCP connection, so one lost segment stalls every concurrent stream, including ones whose bytes already arrived. Six parallel HTTP/1.1 connections actually absorb loss better in that scenario, which is one of the few places the older protocol wins.

QUIC fixes this by moving stream framing above UDP. Each stream is ordered independently, so loss on stream A delivers nothing to stream A and blocks nothing on stream B. It also collapses the handshake:

PathRound trips to first application byte
TCP + TLS 1.23 (1 TCP, 2 TLS)
TCP + TLS 1.32 (1 TCP, 1 TLS)
QUIC, first visit1
QUIC, 0-RTT resumption0
TCP + TLS 1.3 through a proxy4 or more (proxy TCP, CONNECT or SOCKS negotiation, origin TCP, origin TLS)

That last row is the one people forget. Every proxy hop adds its own handshake before the origin handshake even starts. Work it through on a 40 ms client-to-proxy leg and a 90 ms proxy-to-origin leg: 40 ms for the TCP handshake to the proxy, 40 ms for the CONNECT or SOCKS negotiation, 90 ms for the origin TCP handshake, 90 ms for the origin TLS 1.3 handshake. That is 260 ms of pure setup before a single byte of HTML moves. The leg latencies are illustrative, so substitute your own measured RTTs; the shape of the sum does not change. Connection reuse and keep-alive are worth more than any header tweak on latency-sensitive workloads, precisely because they amortise that cost across requests.

Real-Time Media and Game Traffic

Anything that would rather drop a packet than wait for it uses UDP, and none of it traverses an HTTP proxy:

  • Voice and video. RTP payloads over UDP. A 200 ms retransmission of an audio frame is useless, because the moment to play it has passed.
  • Game netcode. Position and input updates on a fixed tick. Valve documents Source engine servers at 66 ticks per second by default, with some titles at 30. The next update supersedes the lost one, so retransmission actively hurts.
  • STUN and TURN. NAT traversal signalling on UDP 3478 and 5349.
  • NTP. UDP 123, and clock skew is itself a fingerprinting surface.
  • Syslog, SNMP, and most telemetry agents. UDP 514 and 161.

If your workload is any of these, an HTTP proxy is not a slower option, it is not an option. You need SOCKS5 with a verified UDP ASSOCIATE implementation, a VPN tunnel, or a purpose-built relay.

How to Test Whether a Proxy Actually Supports UDP

Provider marketing says "SOCKS5 supported" and usually means "CONNECT works." Test it yourself. The cleanest check is a DNS query, because a resolver is the easiest UDP endpoint to reach that gives an unambiguous answer.

curl cannot do this. It speaks SOCKS5 for TCP only, so a green result from curl --socks5-hostname tells you nothing about UDP. Use PySocks:

# pip install PySocks
import socket, struct, socks

def udp_probe(proxy_host, proxy_port, user, password, timeout=6):
    s = socks.socksocket(socket.AF_INET, socket.SOCK_DGRAM)
    s.set_proxy(socks.SOCKS5, proxy_host, proxy_port,
                username=user, password=password)
    s.settimeout(timeout)

    # Minimal DNS query: A record for example.sparkproxy.io
    txn = b"\x2a\x2a"
    header = txn + b"\x01\x00\x00\x01\x00\x00\x00\x00\x00\x00"
    qname = b"".join(bytes([len(p)]) + p.encode()
                     for p in "example.sparkproxy.io".split(".")) + b"\x00"
    query = header + qname + b"\x00\x01\x00\x01"

    s.sendto(query, ("1.1.1.1", 53))
    data, _ = s.recvfrom(512)
    answers = struct.unpack(">H", data[6:8])[0]
    return len(data), answers

try:
    size, answers = udp_probe("proxy.sparkproxy.io", 1080, "user", "pass")
    print(f"UDP ASSOCIATE works: {size} bytes back, {answers} answer records")
except socks.SOCKS5Error as e:
    print(f"Proxy refused the association: {e}")   # 0x07 = command not supported
except socket.timeout:
    print("Association accepted but no datagram returned (broken return path)")

Three outcomes, three different diagnoses. The SOCKS5Error means the server is honest and TCP-only. The timeout is the dangerous one: the control channel said yes and the data path is dead, usually NAT on the relay side. Only the first case is real UDP support.

Node has an equivalent with the socks package, which hands back the relay endpoint you write datagrams to:

// npm i socks
const dgram = require('node:dgram');
const { SocksClient } = require('socks');

const info = await SocksClient.createConnection({
  proxy: { host: 'proxy.sparkproxy.io', port: 1080, type: 5,
           userId: 'user', password: 'pass' },
  command: 'associate',
  destination: { host: '0.0.0.0', port: 0 },
});

console.log('relay endpoint:', info.proxy.host, info.proxy.port);
// Keep info.socket (the TCP control connection) OPEN or the association dies.
const udp = dgram.createSocket('udp4');

These are the SOCKS5 reply codes worth recognising when you read a failure:

ReplyMeaningWhat it tells you
`0x00`succeededassociation created, still needs a data-path test
`0x02`not allowed by rulesetUDP is disabled by policy on your plan
`0x03`network unreachablerelay has no route to the destination
`0x05`connection refuseddestination rejected it
`0x07`command not supportedthe server is TCP-only, full stop
`0x08`address type not supportedoften means no IPv6 or no domain-name targets

Confirm from the other side with a capture on the client while the probe runs:

sudo tcpdump -ni any 'host proxy.sparkproxy.io and udp' -c 20

Datagrams going to the relay port with answers coming back means the path is genuinely open. Only TCP on 1080 and nothing else means the association never carried anything.

Proxying UDP Inside HTTP: MASQUE and connect-udp

The gap between "HTTP proxies are TCP-only" and "the modern web runs on UDP" was obvious enough to the IETF that they closed it. RFC 9298, "Proxying UDP in HTTP," published in August 2022, defines an extended CONNECT with :protocol = connect-udp that carries UDP payloads as HTTP Datagrams (RFC 9297). The request targets a URI template such as https://proxy.sparkproxy.io/.well-known/masque/udp/{target_host}/{target_port}/, and over HTTP/3 those datagrams ride QUIC's own DATAGRAM frames, so there is no TCP anywhere in the path. RFC 9484 extends the same idea to full IP packets with connect-ip.

None of this is theoretical. It is the transport behind Apple's iCloud Private Relay and behind several commercial VPN products. What it is not, as of 2026, is available from mainstream proxy providers or configurable in Chrome and Firefox as a general proxy setting. There is no --proxy-server=masque:// flag. Client support lives in libraries and purpose-built apps.

The practical takeaway: if a provider tells you their HTTP proxy carries UDP, ask which mechanism. If the answer is not connect-udp with a URI template, or SOCKS5 UDP ASSOCIATE, there is no mechanism.

Choosing a Transport for Your Workload

WorkloadNeeds UDP?Recommended setup
HTML scraping, API callsNoHTTP or HTTPS proxy, or SOCKS5 CONNECT
Browser automation (Playwright, Puppeteer)No, disable WebRTCHTTP proxy, WebRTC turned off
SERP and price monitoring at scaleNoManaged Scraping API, no transport to manage
DNS enumeration from the exit IPYesSOCKS5 with verified UDP ASSOCIATE, or `dig +tcp`
Multi-account browsing with WebRTC onYesSOCKS5 UDP, or `disable_non_proxied_udp` and accept no media
Voice, video, conferencingYesSOCKS5 UDP or a VPN tunnel
Game clients, real-time netcodeYesSOCKS5 UDP or a VPN tunnel
Testing a site's HTTP/3 behaviour specificallyYesRun the browser at the exit, not the traffic through a tunnel

The short version for most data-collection work: you do not need UDP, you need to make sure the UDP you are not using cannot escape. Force hostname resolution at the proxy with socks5h or CONNECT, disable WebRTC, and accept the HTTP/2 fallback. For anything real-time, verify UDP ASSOCIATE with an actual datagram before you build on it.

Frequently asked questions

FAQ

No. Both HTTP proxy modes, absolute-form requests and CONNECT tunnels, open a TCP socket to the origin, and the CONNECT request line has no field for requesting a datagram association. The only HTTP-based mechanism for UDP is connect-udp from RFC 9298, which mainstream proxy providers and browsers do not expose.

No, and that is the most common wrong assumption in the tcp vs udp proxies discussion. UDP ASSOCIATE is command 0x03 in RFC 1928, and a large share of commercial SOCKS5 endpoints answer it with reply code 0x07, "command not supported." Some accept the association and still fail to return datagrams because of NAT on the relay side, so test with a real query rather than trusting the reply byte.

Not if the hostname is resolved at the proxy. Use socks5h:// in curl and requests, or let an HTTP proxy resolve the name from the CONNECT line, and your machine never sends a query to port 53. Leaks happen with plain socks5://, with system proxy settings that only cover the browser, and with tools that resolve before dialling.

A TCP-only proxy stops QUIC, so the client falls back to HTTP/2 over TCP with no visible error. On its own that is a weak signal, since many corporate networks block UDP 443 too, but a client claiming to be current Chrome that never negotiates HTTP/3 across thousands of requests adds one more inconsistency to a fingerprint that already includes TLS shape and header order.

Launch Chrome with --force-webrtc-ip-handling-policy=disable_non_proxied_udp, or set media.peerconnection.ice.proxy_only to true in Firefox. WebRTC then refuses any UDP path that does not go through the proxy. With a TCP-only proxy the call fails to connect rather than leaking, which is the correct outcome. For scrapers, disable WebRTC entirely.

Per packet, yes, because there is no handshake, no retransmission wait, and no head-of-line blocking behind a lost segment. Through a SOCKS5 relay you still pay the 10-byte SOCKS UDP header on every IPv4 datagram plus the extra hop, so the win comes from loss behaviour and setup cost rather than raw throughput.

Special Discount ยท 20% off

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

Claim Discount

About the Author

This guide was written by the SparkProxy Technical Team. We build and operate residential, ISP, datacenter, and mobile proxy networks along with the SparkProxy Scraping API, and we spend our days on exactly the transport-layer details above: SOCKS5 handshakes, DNS resolution paths, TLS and QUIC negotiation, and the failure modes that only show up under load. Every protocol claim here is traceable to its RFC, and every code sample was written against tools we run in production. Questions or corrections: support@sparkproxy.io.

Keep reading

Related articles

Understanding Proxy Timeouts and Retry Logic

Understanding Proxy Timeouts and Retry Logic

Proxy timeouts fail at seven layers, not one. Learn what DNS, connect, TLS, CONNECT, TTFB and read timeouts measure, and how to budget them across retries.

SparkProxyยทProxy Basic
Proxy Failover and Redundancy: Design for Failure

Proxy Failover and Redundancy: Design for Failure

Proxy failover means moving work off a failing component. Learn the five failure modes, circuit breakers, multi-provider ASN traps, and RTO/RPO for scrapers.

SparkProxyยทProxy Basic