Skip to content

WebSocket vs TCP: How WebSocket Sits on Top of TCP

The most common misconception: WebSocket is some alternative to TCP. It is not. Every WebSocket connection is a TCP connection. The bytes flow over the same reliable, ordered stream that TCP has always provided.

What WebSocket adds is a protocol layer on top of that TCP connection. It defines how to frame messages, how to open and close connections, and how to interoperate with HTTP infrastructure.

Here is what actually happens when you open a WebSocket connection:

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ Your Application β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚ WebSocket (RFC 6455) β”‚ ← message framing, ping/pong
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚ HTTP (upgrade) β”‚ ← handshake only, then gone
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚ TLS (optional) β”‚ ← wss:// connections
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚ TCP β”‚ ← reliable, ordered byte stream
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚ IP β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

The HTTP layer is only used once, during the opening handshake. The client sends an HTTP Upgrade request. The server responds with 101 Switching Protocols. After that, HTTP is out of the picture. The rest of the connection is WebSocket frames sent directly over TCP (or TLS over TCP).

TCP gives you a byte stream. WebSocket gives you messages. That distinction solves real problems.

TCP has no concept of message boundaries. Send two 100-byte messages on a raw TCP socket. The receiver might get one 200-byte chunk, three chunks of 80, 70, and 50 bytes, or any other combination. You must implement your own length-prefix or delimiter protocol to reconstruct messages.

WebSocket handles this. Every send() produces exactly one message event on the other side.

// With WebSocket: one send = one message. Always.
ws.send(JSON.stringify({ type: 'position', x: 10, y: 20 }));
// With raw TCP: you need your own framing
// 4-byte length prefix + payload
const payload = Buffer.from(JSON.stringify({ type: 'position', x: 10, y: 20 }));
const frame = Buffer.alloc(4 + payload.length);
frame.writeUInt32BE(payload.length, 0);
payload.copy(frame, 4);
socket.write(frame);

WebSocket’s upgrade handshake looks like a normal HTTP request to proxies, CDNs, and load balancers. Raw TCP connections on arbitrary ports get blocked by corporate firewalls. WebSocket on port 443 passes through because the initial handshake is valid HTTP.

Browsers expose a WebSocket API. They do not expose raw TCP sockets. Letting arbitrary JavaScript open TCP connections to any host would let malicious scripts port-scan internal networks, connect to databases, and bypass firewalls. WebSocket’s origin-based security model prevents this.

WebSocket defines close codes (1000 for normal, 1001 for going away, 1008 for policy violation). Raw TCP has FIN and RST but no application-level reason. When a connection drops, you cannot tell the other side why.

WebSocket is not just β€œTCP plus extras.” It also removes significant overhead compared to HTTP.

HTTP/1.1 requestWebSocket frame
Headers per message200-800 bytes (cookies, auth, etc.)0 bytes
Frame overheadN/A2-14 bytes
DirectionClient-initiated onlyEither direction
ConnectionNew or keep-alive poolSingle persistent

An HTTP request carries headers on every single request. Cookies, Authorization, Accept, Content-Type β€” these add up. A typical request header block is 400-800 bytes. Over 1,000 messages per second, that is 400-800 KB/s of pure overhead. A WebSocket frame header is 2 bytes for small messages, 4 bytes for messages up to 65,535 bytes, and 10 bytes for larger payloads. Client-to-server frames add 4 bytes for the masking key, bringing the total to 6-14 bytes.

People ask β€œwhat is the overhead of WebSocket?” Here are the exact byte counts from RFC 6455:

WebSocket frame header structure:
0 1
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7
+-+-+-+-+-------+-+-------------+---+
|F|R|R|R| opcode|M| Payload len |...|
|I|S|S|S| (4) |A| (7) | |
|N|V|V|V| |S| | |
| |1|2|3| |K| | |
+-+-+-+-+-------+-+-------------+---+
Small message (≀125 bytes): 2 bytes header
Medium message (≀65535 bytes): 4 bytes header
Large message (>65535 bytes): 10 bytes header
Client β†’ Server adds 4-byte masking key:
Small: 6 bytes Medium: 8 bytes Large: 14 bytes

Compare this to TCP itself, which adds no application-layer framing at all. TCP is a byte stream β€” zero framing overhead, but zero message boundaries. The 2-14 bytes WebSocket adds per frame is the cost of getting discrete messages instead of a raw stream.

The β€œWebSocket is faster than TCP” misconception

Section titled β€œThe β€œWebSocket is faster than TCP” misconception”

You will see benchmarks claiming WebSocket is faster than some other protocol. That is a comparison against HTTP or SSE, not against TCP. WebSocket cannot be faster than TCP because it runs on TCP. Every WebSocket byte travels through the TCP stack.

What people actually mean: WebSocket has less overhead than HTTP for repeated messages because it eliminates per-request headers. After the initial handshake, a WebSocket message adds 2-6 bytes of framing. An equivalent HTTP request adds hundreds of bytes of headers. For high-frequency communication, that difference is real.

But compared to raw TCP? WebSocket is strictly slower. It adds framing, masking (client-to-server), and protocol processing that raw TCP does not have. The overhead is tiny --- microseconds per message --- but it exists.

WebSocket inherits TCP’s head-of-line blocking problem. If one TCP packet is lost, every subsequent packet waits for the retransmission, even if those packets contain completely independent messages.

Consider a chat application sending messages from three different channels over one WebSocket connection. A packet carrying a message from channel A is lost. Messages from channels B and C are already buffered in the kernel. They wait. TCP will not deliver out-of-order data to the application.

For most applications, this is fine. Packet loss on modern networks is under 0.1%, and retransmission takes 10-30ms. But for latency-sensitive applications β€” real-time gaming, live audio, financial feeds β€” that stall matters.

Services like Ably, Pusher, and PubNub handle this at the infrastructure level by maintaining multiple connections across regions and using message ordering at the application layer rather than relying solely on TCP ordering. But the head-of-line blocking at the protocol level is inherent to TCP, and therefore inherent to WebSocket.

WebTransport uses QUIC, which runs over UDP. QUIC provides independent streams β€” a lost packet on stream A does not block streams B and C. If head-of-line blocking is a real problem for your application (measure first, don’t assume), WebTransport is the better protocol choice.

The trade-off: WebTransport has limited browser support compared to WebSocket. Chrome supports it. Firefox supports it. Safari added support in 2025. But the ecosystem of libraries, services, and documentation is years behind WebSocket.

WebSocket exists because browsers need it. If your clients are not browsers, you have more options.

Server-to-server communication. Two backend services talking to each other do not need HTTP proxy traversal or browser security. Raw TCP (or gRPC, which uses HTTP/2 over TCP) removes the WebSocket framing overhead.

Custom binary protocols. Database wire protocols (PostgreSQL, MySQL, Redis), message brokers (AMQP, MQTT over TCP), and game servers define their own framing. Adding WebSocket on top adds complexity without benefit.

When you need UDP. Multiplayer game state, voice/video media streams, and DNS queries need UDP’s fire-and-forget semantics. WebSocket cannot provide this. Use raw UDP, QUIC, or WebTransport depending on your client constraints.

Maximum throughput. If you are moving gigabytes between servers and every microsecond matters, raw TCP avoids WebSocket’s per-frame masking and framing. In practice, this matters only at extreme scale β€” the overhead is single-digit microseconds per message.

A question that comes up: β€œWhy can’t browsers just give me a TCP socket?”

Because it would break the web security model. JavaScript runs in a sandbox. If a script on evil-site.com could open a TCP connection to 192.168.1.1:5432, it could talk to your internal PostgreSQL database. Or scan your local network. Or connect to any service that assumes network-level access control.

WebSocket prevents this through the HTTP handshake. The server must explicitly accept the connection by responding with 101 Switching Protocols and validating the Origin header. This puts the server in control of which origins can connect β€” the same model as CORS for HTTP.

WebSocket and UDP are fundamentally different because WebSocket sits on TCP:

PropertyWebSocket (TCP)UDP
DeliveryGuaranteedBest-effort
OrderingGuaranteedNone
ConnectionPersistent, statefulConnectionless
FramingBuilt-in messagesDatagrams (you frame)
Browser accessYes (WebSocket API)No (WebTransport for QUIC)
Head-of-line blockingYes (TCP)No

If you need guaranteed delivery and ordering, WebSocket (via TCP) gives you that for free. If you need low-latency delivery where stale data is worse than missing data, UDP-based protocols are the right choice.

TCP, exclusively. Every WebSocket connection is a TCP connection with WebSocket framing on top. The confusion comes from WebTransport, which uses QUIC (built on UDP). WebSocket and WebTransport are separate protocols with separate browser APIs. There is no UDP mode for WebSocket.

No. WebSocket runs on TCP, so it cannot be faster than the transport underneath it. WebSocket is faster than HTTP for repeated messages because it eliminates per-request headers. But compared to raw TCP, WebSocket adds 2-14 bytes of framing overhead per message. The difference is negligible for most applications.

Four things: message framing (TCP is a byte stream with no boundaries), an HTTP-compatible handshake (works through proxies and firewalls), browser access via the JavaScript WebSocket API, and a close handshake with application-level status codes. See the protocol stack section for the full layer breakdown.

When you control both endpoints and do not need browser compatibility or HTTP proxy traversal. Server-to-server communication, custom binary protocols (database wire formats, game protocols), and high-throughput data pipelines are all cases where raw TCP or gRPC makes more sense than WebSocket.

A lost TCP packet blocks delivery of all subsequent packets on that connection, even if they carry independent messages. This is a TCP limitation that WebSocket inherits. For most applications, retransmission takes 10-30ms and is barely noticeable. For latency-critical systems, WebTransport over QUIC provides independent streams that avoid this problem.