AnyProxy500+ sites
Get Pro
Product18 Aug 2026 · 11 min read

What is the Wisp protocol? TCP over one WebSocket, explained

Wisp carries many TCP and UDP connections over a single WebSocket, using a 5-byte header. Here is how the protocol works, packet by packet, and why moving TLS into the browser changes what a web proxy can reach.

SR
Sam ReyesBackend & regions
Product

Every browser-based proxy runs into the same wall on day one: a web page cannot open a network connection. Not a real one. There is no connect() in JavaScript, no socket API, no way for code in a tab to reach out to port 443 on a machine of its choosing. The browser sandbox exists precisely to prevent that.

Which leaves a narrow set of doors. fetch() speaks HTTP and only HTTP, and CORS decides where it may knock. WebSocket gives you one persistent two-way byte pipe — but only to a server that already agreed to speak WebSocket with you. That is the entire toolkit.

Wisp is what happens when you take the second door seriously and build a network stack behind it.

What is the Wisp protocol?

Wisp is a lightweight multiplexing protocol that carries many TCP and UDP connections over a single WebSocket, using a 5-byte header per message. A Wisp client running in a browser tab asks a Wisp server to open sockets on its behalf, then shuttles raw bytes back and forth over the one WebSocket connection they share.

The specification was written by @ading2210 at Mercury Workshop and published under a Creative Commons licence. There are two major versions: v1, deliberately trivial to implement, and v2, which adds a negotiated handshake, authentication, and protocol extensions. Server and client implementations exist in JavaScript, Python, Rust, Go, and C++.

The useful mental model is not “a proxy protocol”. It is a fake network card for a browser tab.

BROWSER TAB                 PROXY SERVER              THE INTERNET

 [ wisp client ]  ══ 1 WebSocket, port 443 ══  [ wisp server ]
                            stream 1  ─── TCP ───>  example.com:443
                            stream 2  ─── TCP ───>  reddit.com:443
                            stream 3  ─── UDP ───>  1.1.1.1:53

Multiplexing: many connections, one pipe

Multiplexing means many independent conversations sharing one physical connection. In Wisp each conversation is called a stream, and every stream corresponds to exactly one TCP or UDP socket on the server side.

The client picks a 32-bit stream ID for each new stream and stamps it on every message belonging to that stream. The server keeps a table mapping IDs to real sockets. That is the whole trick — and it is what makes the protocol practical rather than merely possible.

Consider the alternative, which is what Wisp’s ancestor wsproxy did: one WebSocket per destination, with the target host and port in the URL path. A modern page pulls from twenty or thirty hosts. That becomes thirty WebSocket handshakes, thirty TLS negotiations, thirty connections for a firewall to notice and a browser connection limit to throttle.

With Wisp it is one connection, opened once, reused for everything. Stream setup costs a single message.

Inside a Wisp packet

Every Wisp message, regardless of type, starts with the same five bytes.

byte:  0        1   2   3   4      5 ...
      ┌────────┬───────────────────┬──────────────────┐
      │  type  │     stream ID     │     payload      │
      │ uint8  │   uint32 little-  │      bytes       │
      │        │     endian        │                  │
      └────────┴───────────────────┴──────────────────┘

uint8 and uint32 mean an unsigned integer one byte and four bytes wide. Little-endian means the least significant byte comes first, so port 443 travels as BB 01, not 01 BB — get that backwards and nothing works, which is the traditional first bug in every fresh implementation.

Five bytes of framing. For comparison, a single HTTP request header block is typically several hundred bytes. This is why Wisp can carry a video stream without the framing showing up in your bandwidth graph.

One reservation matters: stream ID 0 is not a stream. It is the control channel, used for the handshake and connection-wide messages.

The five packet types

Type Name Direction Purpose
0x01 CONNECT client → server Open a socket to a host and port
0x02 DATA both Raw socket bytes
0x03 CONTINUE server → client Flow control credit
0x04 CLOSE both Tear down a stream, with a reason code
0x05 INFO both Version and supported extensions

CONNECT carries one byte for stream type (0x01 TCP, 0x02 UDP), two bytes of port, and the hostname as a UTF-8 string. Note where the hostname is resolved: on the server. The browser never looks up the destination, so DNS-level filtering on the local network has nothing to act on.

DATA is the payload path, and it is deliberately boring — the packet body is the socket bytes, nothing wrapped, nothing encoded. The server keeps a separate first-in-first-out queue per TCP stream so ordering survives.

CONTINUE is the interesting one, because it solves a problem that sinks naive implementations. A browser can generate data far faster than a slow origin server can absorb it. Without a brake, the proxy server’s memory fills with buffered bytes and the process dies. Wisp uses a credit system:

server → CONTINUE(128)    client credit = 128
client → DATA             credit 127
client → DATA             credit 126
         ... 126 more ...
         credit = 0       client MUST stop sending
server → CONTINUE(128)    credit refilled, client resumes

The count is in packets, not bytes. A well-behaved server tops the credit up early, before the client ever stalls. UDP streams get no credit accounting at all — datagram loss is acceptable by definition.

CLOSE carries a one-byte reason. Closing happens either way; the code is there so failures are diagnosable rather than mysterious. Some are ordinary (0x02 voluntary, 0x03 network error, 0x44 connection refused), and two are worth knowing as a user of any Wisp service: 0x48 means the proxy is intentionally blocking that destination, and 0x49 means you are being rate-limited. A client that surfaces those codes can tell you why something failed instead of showing a generic spinner.

INFO announces protocol version and a list of supported extensions — optional features such as UDP support, password authentication, Ed25519 key authentication, a server welcome message, and stream-open confirmation. Only extensions present in both sides’ INFO packets may be used.

The handshake, and one genuinely elegant trick

CLIENT                                        SERVER
  │  GET /wisp/  Upgrade: websocket
  │  Sec-WebSocket-Protocol: <any value>  ──────>
  │                                       <────── 101 Switching Protocols
  │                                       <────── INFO   (stream 0)
  │  INFO + credentials (stream 0)        ──────>
  │                                       <────── CONTINUE (stream 0) = accepted
  │                                              or CLOSE = rejected
  │  CONNECT / DATA on real stream IDs …

Two details do a lot of work here.

First, version selection rides on the mere presence of the Sec-WebSocket-Protocol header. Its value is ignored. Present means the client speaks v2; absent means fall back to v1. No version field to disagree about.

Second, and this is the part worth stealing: if the client’s first packet from the server is CONTINUE rather than INFO, the server is a v1 server, and the client silently drops to v1. Backward compatibility with zero extra round trips and zero negotiation. Protocols rarely get to age this gracefully.

Why Wisp matters: it moves TLS into the browser

Everything above is competent engineering. This next part is the reason Wisp changed what browser proxies can reach.

A conventional web proxy fetches pages on your behalf. Your browser asks the proxy, the proxy’s server code performs the TLS handshake with the destination, reads the HTML, rewrites the links, and hands the result back. We described that flow in detail in how a web proxy works.

The problem is who performs that handshake.

CONVENTIONAL PROXY
browser ──HTTP──> proxy server ──TLS handshake by the server──> origin
                       ▲ server decrypts, rewrites, re-encrypts

WISP TUNNEL
browser ──WebSocket──> proxy server ──raw TCP bytes──> origin
   ▲ TLS handshake happens HERE,          ▲ server relays bytes it
     inside the browser                     cannot read

Every TLS client leaves a signature in its opening message — the cipher list, the order of extensions, the curves it offers. Hash that and you get a JA3 or JA4 fingerprint, and it identifies the software connecting with uncomfortable precision. Real Chrome has one. Node.js has a very different one. Anti-bot services keep catalogues of both.

So a server-side proxy is legible before it says a word. The fingerprint says “this is a Node process”, the header ordering agrees, and the HTTP/2 frame pattern confirms it. That is why so many proxies meet a challenge page on protected sites.

Over a Wisp tunnel, the TLS handshake is performed by a TLS stack compiled to WebAssembly and running in the browser tab. The destination sees a handshake produced in a real browser environment. The proxy server, meanwhile, is a dumb byte relay — it moves encrypted bytes it has no key for.

How AnyProxy uses Wisp

We run Wisp as one of two paths, not as the whole product.

The server side. A Wisp endpoint at /wisp/ accepts WebSocket upgrades. Every connection must carry a session ID that resolves to a live session record, so the endpoint is not an open relay — the same authentication model our other routes use. On top of that sit the guards that any Wisp operator needs: direct-IP, loopback, and private-range destinations are refused, stream counts are capped per host and globally, and a single environment flag disables the endpoint outright if we need it gone.

The browser side. A ServiceWorker loads a Rust TLS stack compiled to WebAssembly — the epoxy-tls client — and points it at wss://anyproxy.site/wisp/. From that point the worker can issue requests whose TLS is negotiated inside the tab.

The routing decision. We do not send everything down the tunnel. Our standard path is faster, streams responses without buffering, and rewrites HTML server-side where that work belongs. So a detector watches upstream responses for challenge-page signatures, and when it sees one, that host gets marked and its subsequent requests are rerouted through the tunnel. Hosts that were never a problem never pay the tunnel’s cost.

That cost is worth naming: a WebAssembly TLS stack is a real download and a real startup delay. Spending it on every request to sites that load fine anyway would make the product worse, not better.

What Wisp does not fix

Wisp is a transport. Transports do not solve every layer above them, and a clear-eyed list matters more than a pitch.

Problem Does Wisp help?
DNS-level network filtering ✓ Destination is resolved server-side
IP and firewall blocks on the local network ✓ Only one connection to the proxy is visible
TLS fingerprint checks at the destination ✓ Handshake comes from the browser
Non-HTTP protocols in a browser ✓ Raw TCP and UDP, so SSH, IRC, and games work
Server IP reputation ✗ The origin still sees our server’s address
JavaScript and behavioural challenges ✗ Different layer entirely
Interactive challenge widgets ✗ Not a transport problem
Per-IP rate limits ✗ Unchanged

The IP point deserves emphasis, because it is the one most often glossed over. The Wisp server opens the TCP connection, so the destination sees the server’s address as the source. Wisp fixes how your connection looks; it does not change where it comes from.

There is also a live platform gap on our side: the WebAssembly memory configuration the TLS client needs currently crashes Safari on iOS, so the tunnel path is unavailable there until we work around it. We would rather write that down than let you discover it.

Should you build on Wisp?

Reach for it when you need real sockets in a browser — a terminal client, an emulator that wants a network card, a game protocol, anything that is not HTTP. The ecosystem is a decent argument by itself: v86 uses Wisp to give an in-browser x86 emulator networking, Puter uses it as an OS-level transport, and Wispcraft uses it so a browser can speak the Minecraft protocol to real servers.

Skip it when plain HTTP proxying already does the job. If you are fetching and rewriting pages server-side and it works, a 5-byte multiplexing layer and a WebAssembly TLS stack are complexity you have not yet earned.

The revolutionary part of Wisp is not the framing. It is that a 5-byte header was enough to move TLS termination from the server to the browser — and that single relocation changed which sites a browser-based proxy can reach.

If you just want the page to load

None of this needs to be your problem. Open anyproxy.site, paste the address, and the routing decision happens without you. If the site behind it turns out to need the tunnel, the tunnel is what you get.

The plumbing is only interesting until it works.

Try it on the site you're blocked from

SR
Sam ReyesBackend & regions

Runs the AnyProxy edge. Spends their day shaving milliseconds off the path between you and a blocked page.

Keep reading

Guides
Guides · 6 minHow a web proxy actually works (the request flow, step by step)
Guides
Guides · 4 minHow web filters work: DNS, IP, and DPI (plain-English guide)
Privacy
Privacy · 5 minWhat a proxy can and cannot hide