Try 500 MB of US mobile proxy data free for 30 days.Start free trial
October 30, 202614 min read

WebSocket Over Proxy: How It Works and How to Fix It

Geometric tunnel showing WebSocket proxy passage

A WebSocket can run through a proxy in two ways. With a forward proxy, your client asks the proxy to open a TCP tunnel to the server with an HTTP CONNECT request, and the WebSocket handshake runs inside that tunnel. With a reverse proxy, the proxy sits in front of your server and must be told to pass the Upgrade and Connection headers and to keep idle connections open. Most failures come from one of three places: stripped headers, short idle timeouts, or a proxy that cannot tunnel.

This guide covers both directions, with working client code, a debugging checklist, and what changes when the goal is geo-targeted data collection through real carrier IPs.

TL;DR

  • Forward proxy (client side): the client sends CONNECT host:port to the proxy, gets a tunnel, then performs the normal WebSocket handshake through it. SOCKS5 proxies work at the TCP level and can carry the same traffic.
  • Reverse proxy (server side): pass Upgrade and Connection explicitly and raise the read timeout above your ping interval, or the connection drops on a timer.
  • wss:// is the safer default: TLS stops intermediaries from rewriting the upgrade headers.
  • Location: the target server sees the proxy's exit IP, so the city and carrier of that IP decide the apparent location of your WebSocket session.

Table of Contents

Forward proxy or reverse proxy: which one are you dealing with?

The phrase "WebSocket over proxy" covers two different setups, and the fixes do not overlap.

A forward proxy acts on behalf of clients and can hide the client's identity. A reverse proxy sits in front of servers, hides the server's identity, and can balance load, cache static content or compress content, as the MDN guide to proxy servers and tunneling describes it.

Forward proxyReverse proxy
Who configures itThe client (your scraper, app or browser)The server operator
Who knows about itThe clientOnly the server side; clients are unaware
Typical goalChange the visible IP, route outbound traffic, apply policyTLS offload, load balancing, routing
Key WebSocket settingCONNECT tunnel supportUpgrade and Connection headers, timeouts
Typical failureProxy refuses CONNECT or requires authHandshake returns plain HTTP, or idle drop

The Nginx WebSocket documentation draws the same line: forward-proxy clients can use CONNECT to get around the hop-by-hop header problem, while reverse-proxy clients do not know the proxy exists, so the proxy needs special handling.

If you are building a scraper or a data pipeline, you are almost always on the forward-proxy side. If you are deploying a chat, trading or dashboard backend, you are on the reverse-proxy side. The rest of this guide takes them in that order.

How a WebSocket crosses a proxy

A WebSocket starts life as an ordinary HTTP/1.1 request that asks the server to switch protocols. The client sends Upgrade: websocket and Connection: Upgrade, plus a key. If the server agrees, it answers 101 Switching Protocols and both sides stop speaking HTTP and start exchanging frames over the same TCP connection.

That design is the source of every proxy problem. Upgrade and Connection are hop-by-hop headers, meant to apply to a single network hop. A proxy that follows normal HTTP rules may drop them, and the server then sees a plain request and replies with an ordinary page instead of a 101. Nginx handles this by setting up a tunnel once the client requests a protocol switch and the proxied server returns 101, but only when you tell it to pass the two headers explicitly (Nginx docs).

A forward proxy avoids the problem differently. Under RFC 6455, a client configured to use a proxy connects to that proxy and asks for a TCP connection to the destination host and port. Once the proxy returns success, it stops interpreting the bytes. The handshake and every later frame pass through untouched. For wss://, the TLS session is also negotiated end to end inside the tunnel, so the proxy sees only encrypted bytes.

In practice the exchange looks like this:

  1. Client opens a TCP connection to the proxy.
  2. Client sends CONNECT echo.example.com:443 HTTP/1.1 (with Proxy-Authorization if the proxy needs credentials).
  3. Proxy answers 200 Connection established.
  4. Client starts TLS to the target through the tunnel, then sends the GET with Upgrade: websocket.
  5. Server returns 101, and frames flow.

If you want the full mechanics of step 2 and how to debug it, we wrote a separate walkthrough on how the HTTPS proxy CONNECT tunnel works.

Connecting a WebSocket client through a forward proxy

Support lives in the client library's transport layer, so the first question is whether your library can dial through a proxy at all. Two common examples:

Python

The websockets library reads proxy settings automatically. Its proxy documentation lists the order it checks: the wss_proxy or ws_proxy environment variables, a SOCKS proxy configured in the operating system, then an HTTP proxy from the system or the https_proxy variable. You can override that with the proxy argument of connect(), and credentials in the proxy URL (http://user:password@proxy:8080/) are used for authentication. SOCKS proxies need a third-party library installed.

import asyncio
from websockets.asyncio.client import connect

async def main():
    async with connect(
        "wss://echo.example.com/socket",
        proxy="http://USER:[email protected]:8080",
    ) as ws:
        await ws.send("ping")
        print(await ws.recv())

asyncio.run(main())

The same proxy= idea applies to HTTP clients. If your pipeline mixes REST calls and WebSockets, our HTTPX proxy setup guide shows how to keep one proxy configuration for both.

Node.js

The ws package accepts an agent, and a proxy agent such as https-proxy-agent supplies the CONNECT logic (example setup):

const WebSocket = require('ws');
const { HttpsProxyAgent } = require('https-proxy-agent');

const agent = new HttpsProxyAgent('http://USER:[email protected]:8080');
const ws = new WebSocket('wss://echo.example.com/socket', { agent });

ws.on('open', () => ws.send('ping'));
ws.on('message', (m) => console.log(m.toString()));

Check the agent package's current export name and version before copying, since it has changed between releases.

HTTP proxy or SOCKS5?

Both can carry a WebSocket, for different reasons. An HTTP proxy works for wss:// only if it supports CONNECT; for plain ws:// it must also forward the upgrade headers intact. SOCKS5 works at the TCP level, so it preserves the byte stream and TLS without caring what is inside (ProxyRoller's compatibility overview). If your client library supports SOCKS5 and your proxy provider offers it, it removes one class of header problems. Our comparison of HTTP vs HTTPS proxy decision rules covers when each protocol fits.

Test the proxy before you blame your code

Before debugging application code, confirm the proxy can tunnel at all:

curl -v --proxy http://USER:[email protected]:8080 \
  --http1.1 \
  -H "Connection: Upgrade" -H "Upgrade: websocket" \
  -H "Sec-WebSocket-Version: 13" \
  -H "Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==" \
  https://echo.example.com/socket

In the verbose output, look for a 200 on the CONNECT line and then 101 Switching Protocols from the server. A 407 means the proxy wants credentials (see fixing 407 proxy authentication errors). A 4xx or 5xx instead of the 101, or a response that is just an HTML page, means the upgrade did not survive.

Configuring a reverse proxy for WebSockets

If the proxy is yours and sits in front of a WebSocket server, the job is to make it transparent to the handshake.

Nginx. Pass the two headers explicitly and use HTTP/1.1 to the upstream. Nginx's own documentation covers the exact directives, and it warns that by default it closes a proxied connection if no data is sent within 60 seconds. You raise proxy_read_timeout, or have the upstream server send periodic WebSocket ping frames (Nginx docs).

Apache. The mod_proxy_wstunnel module has been deprecated since version 2.4.47, because mod_proxy_http now handles protocol upgrades better (Apache documentation). On a current install, check which module your configuration actually relies on before copying an older tutorial.

Envoy. WebSocket termination, with or without TLS, requires upgrade_configs in the HTTP connection manager or on a per-route basis (Envoy WebSocket sandbox).

TLS termination or passthrough. When the proxy terminates TLS, it needs a valid certificate and talks to the upstream over ws:// or a second wss:// hop. In passthrough mode it forwards encrypted bytes and never sees the headers. Pick one deliberately; mixing them per route is where confusing handshake failures begin.

Forwarded headers. Well-behaved reverse proxies strip hop-by-hop headers, append X-Forwarded-For and set X-Forwarded-Proto so the backend can reconstruct the original request (foundation_proxy docs). Your WebSocket server's origin checks and logging depend on these, so confirm it reads them.

Mode and port. Cisco's Multicloud Defense documentation says forward-proxy mode cannot be mixed with other proxy modes on the same port, and that domains with different WebSocket header needs call for separate port configuration (Cisco documentation, 2025). Plan one listener per behavior.

Configuring a reverse proxy for WebSockets — overview diagram

Why WebSockets drop behind a proxy

The symptoms fall into a small set of patterns. Work through them in this order.

1. The handshake returns normal HTTP

NATS documents that standard request/response proxies break WebSocket connections because they strip the Upgrade and Connection headers and do not handle long-lived, idle connections properly (NATS docs). If you get a 200 with HTML, or a 400 from the server, capture the request as the server received it and compare headers.

Switching to wss:// often fixes this. Because the traffic is encrypted, an intermediary cannot alter the upgrade headers (WebSocket.org handshake reference). That only applies when the proxy tunnels with CONNECT rather than intercepting TLS.

2. The connection dies after a fixed time

A drop at exactly 60 seconds, or another round number, means an idle timeout. Compare the proxy's read timeout with the server's keepalive. Socket.IO's guidance is that reverse proxy timeouts must be longer than the sum of the server's pingInterval and pingTimeout, or the proxy closes the connection forcefully (Socket.IO v4 docs). NATS shows Kubernetes Nginx ingress annotations for proxy-read-timeout and proxy-send-timeout set to 3600 seconds for long-lived sessions (NATS docs).

A practical rule: set the application ping to half the shortest idle timeout anywhere on the path, including cloud load balancers you do not control.

3. The handshake times out

Slow or overloaded proxies add latency to connection setup. NATS advises tuning server-side handshake timeouts to allow for it (NATS docs). On the client side, give the open call a timeout of several seconds rather than the default of a tight one, and retry on a fresh proxy session.

4. Blocked on a non-standard port

WebSocket servers on ports other than 80 or 443 can hit firewalls and proxies that only allow web ports (MDN). Many forward proxies also restrict CONNECT to 443. Serving the WebSocket on 443 avoids both.

5. Auth or tunnel errors before the handshake

A 407 or a refused CONNECT happens before any WebSocket logic runs. These are proxy problems, not WebSocket problems, and the fastest route is to find the failing layer: TCP to the proxy, proxy auth, tunnel to the target, TLS, then upgrade. If you build scrapers, the HTTP-first scraping checklist is also worth running, since many pages that seem to need a live socket can be read from a plain HTTP endpoint instead.

Five layers for diagnosing proxy connection failures

Keeping long-lived sessions stable

An ordinary HTTP request can rotate to a new IP on every call. A WebSocket cannot: the connection lives on one TCP tunnel, which means one exit IP, for as long as it is open. That changes how you configure rotation.

  • Use a sticky session for the socket. Pin the IP for the lifetime of the connection, and rotate only when you reconnect.
  • Reconnect with backoff. When a session drops, open a new tunnel, repeat the handshake and resubscribe. Do not retry instantly against the same failing exit.
  • Keep REST and socket traffic consistent. If a site sets a cookie over HTTP and then expects the socket from the same address, use the same sticky session for both. We cover this in session management for stable proxy workflows.
  • Fail over deliberately. Decide in advance when to switch exits, rather than retrying blindly. Our proxy failover strategies describe four patterns that reduce wasted retries.
  • Log the right fields. Record proxy session ID, tunnel result, handshake status and close code for each connection. See what to capture in proxy usage logs.

Geo-targeting a WebSocket connection

Can a WebSocket through a proxy collect location-specific data? Yes, with one condition: the server only sees where the tunnel exits. When your client sends CONNECT through a proxy, the destination's TCP connection comes from the proxy's exit IP, so the apparent country, city and network are those of that IP. The WebSocket protocol adds nothing of its own to the location signal.

That has practical consequences for live-data work such as regional price feeds, local ad verification, or checking what a user in a given city sees on a real-time page:

  • Exit location sets the content. If the site decides what to push over the socket from IP geolocation, you must choose the exit city before you open the connection. Changing it means closing and reconnecting.
  • Network type matters. Sites that classify traffic by network may treat datacenter ranges differently from residential or mobile carrier ranges. Real carrier IPs look like ordinary phone traffic, which is the reason teams choose them for data collection that needs to look like normal users.
  • The HTTP leg and the socket leg should agree. If the page load comes from one city and the socket from another, some sites notice.

Session pinning plus a chosen city is the pattern. Resolve the city at the start, hold the sticky session while the socket is open, and release it when you disconnect.

Why we built for this

We have seen the same pattern in support questions: teams get the tunnel working in a test, then lose sockets in production because rotation, timeouts and authentication were configured for short HTTP calls. Our view is that a WebSocket needs the opposite of a typical scraping setup. It needs a stable exit, predictable idle behavior and clear errors at each layer.

That is why our mobile proxies give you both sticky and rotating sessions, support HTTP, HTTPS and SOCKS5, and expose credentials and usage in a dashboard you manage yourself. You choose a sticky session for a socket and a rotating one for stateless requests, and you can check usage per credential when something looks off. Before moving a real workload, test your own target with a short trial run: open the socket, hold it past your idle timeout, and confirm reconnection behaves as you expect.

— Jon

Run it on real US carrier IPs

If your WebSocket work needs a US location, MaskLabs gives you real mobile carrier IPs in 46 US cities, with sticky and rotating sessions, HTTP, HTTPS and SOCKS5 access, and self-serve credentials with no sales call. Billing is metered per byte, and plans start at $30 per month (Starter), with Basic at $100, Advanced at $500 and Scale at $1,000 per month. A free trial with limited data lets you test your socket against your target before you commit.

Masklabs

See the plans and pricing or start from US mobile proxies on real carrier IPs.

A pre-flight checklist

  1. Confirm which side of the proxy you control: forward (client) or reverse (server).
  2. Test CONNECT with curl and look for a 200 on the tunnel and a 101 from the server.
  3. Prefer wss:// on port 443.
  4. For reverse proxies, pass Upgrade and Connection explicitly and set read and send timeouts above your ping interval plus timeout.
  5. Send application-level pings at half the shortest idle timeout on the path.
  6. Use a sticky session for the socket, and reconnect with backoff.
  7. Choose the exit city before connecting if content depends on location.
  8. Log session ID, tunnel status, handshake status and close code.

FAQ

Does a proxy support WebSockets by default?

Not always. A forward proxy must support the CONNECT method to carry wss://, and for plain ws:// it must pass the upgrade headers unchanged. A reverse proxy usually needs explicit configuration, such as passing Upgrade and Connection in Nginx. Test with curl before assuming.

Is wss:// better than ws:// behind a proxy?

Usually yes. TLS encrypts the handshake, so an intermediary cannot strip or rewrite the upgrade headers. It also removes the plaintext exposure of your data. It also tends to pass firewalls that allow only port 443.

Why does my WebSocket disconnect after about 60 seconds?

That is typically an idle timeout. Nginx closes proxied connections after 60 seconds without data by default. Raise proxy_read_timeout or send ping frames more often than the timeout, and check any load balancer in the path as well.

Can I rotate the IP in the middle of a WebSocket connection?

No. A WebSocket is one long-lived TCP connection, so it keeps the exit IP it started with. To change the IP, close the socket and reconnect through a new session. Use sticky sessions while the socket is open.

Can I choose the location a WebSocket appears to come from?

Yes, by choosing the proxy exit. The server sees the proxy's IP, so a proxy in a given US city makes the connection appear to come from there. Pick the city before you open the connection.

Sources

Recommended