← Back to list

WebSockets Look Simple Until You Have to Run Them in Production

The API is simple. The hard part is operating thousands of long-lived connections.

Basel Issmail in DevOps.dev · 2026-06-06 19:36 · 57 claps · 12.1 min read paywalled
#websocket #software-architecture #system-design-concepts #distributed-systems #software-engineering
Open on Medium ↗
Wiki topics: 🔒 · Cybersecurity 🏛️ · Architecture

The browser API makes WebSockets look cheap. Production proves they are not.

The browser API makes WebSockets look cheap. Production proves they are not.

WebSockets Look Simple Until You Have to Run Them in Production

The API is simple. The hard part is operating thousands of long-lived connections.

Most developers meet WebSockets through a beautiful little demo.

Open a browser. Create a socket. Listen for messages. Send a message. Watch another tab update instantly.

const socket = new WebSocket("wss://example.com/realtime");

socket.onmessage = (event) => {
  console.log("Received:", event.data);
};

socket.send("hello");

It feels almost magical.

One line opens the connection. Another sends a message. A callback receives the response. No refresh. No polling. No request-response ceremony.

That is the part everyone remembers.

But the demo hides the real shift:

With HTTP, the unit of work is usually a request. With WebSockets, the unit of work becomes the connection.

And a connection is not cheap just because the browser API makes it look cheap.

That connection stays alive. It consumes memory. It crosses load balancers. It survives deploys badly. It can silently die. It needs heartbeat logic. It complicates horizontal scaling. It turns “send this message” into a distributed delivery problem.

That is where WebSockets become interesting.

Not in the first five minutes.

In production.

WebSockets are easy to use and surprisingly hard to operate. Not because the protocol is bad. In fact, the protocol is elegant. The hard part is what that persistent connection does to your system once you move beyond the demo.

The Demo Hides the Real Problem

With normal HTTP, the interaction is short-lived. A client asks. A server answers. The connection may close. The server moves on.

This model is easy to scale because the server usually does not need to remember much between requests. If meaningful state lives in a database, cache, session store, or token, any healthy server can usually handle the next request.

WebSockets change that.

Once a client connects, it is attached to a running process. That server now owns a live connection to that user. If user A is connected to server A, you cannot deliver a message to user A by randomly hitting server B.

Server B does not have that socket.

That one detail is the source of most production complexity. A WebSocket system is not just an API endpoint. It is a connection management system.

You need to know who is connected, where they are connected, whether they are still alive, how to deliver messages across multiple servers, and what happens when one of those servers disappears.

So why do we put up with this complexity?

Because for some problems, polling is worse.

Why WebSockets Exist and Why They Start as HTTP

The web was not originally designed for real-time applications. HTTP was built around request and response.

The browser asks:

Do you have this page?

The server responds:

Yes, here it is.

That model works beautifully for documents, images, forms, APIs, and most application traffic. But it becomes awkward when the server needs to speak first.

Imagine a chat application. Your friend sends a message. The server knows about it. But with classic HTTP, the server cannot simply call the browser whenever it wants. The browser has to ask.

So developers used polling.

Every few seconds:

Anything new? Anything new? Anything new?

Most of the time, the answer was no.

Polling works, but it wastes effort. It creates unnecessary requests, headers, server work, and latency.

Then came long polling. Instead of replying immediately with “nothing new,” the server keeps the request open until something happens or it times out. Once the server responds, the client immediately starts another long poll.

That is better, but it is still a workaround. You are still pretending that a long-lived conversation is a series of HTTP requests.

WebSockets changed the model.

Instead of repeatedly asking the server for updates, the client opens one persistent connection. After that, both sides can send messages whenever they need to. The browser can speak. The server can speak. No one has to keep knocking on the door.

But one of the cleverest design decisions in WebSockets is that they do not begin as some strange custom protocol on a strange custom port.

They begin as an HTTP request.

The client sends something like this:

GET /realtime HTTP/1.1
Host: example.com
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: x3JJHMbDL1EzLkh9GBhXDw==
Sec-WebSocket-Version: 13

The important part is this:

Upgrade: websocket
Connection: Upgrade

The client is basically saying:

I know this starts as HTTP, but I would like to upgrade this connection into something else.

If the server agrees, it responds:

HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: HSmrc0sMlYUkAGmm5OPpG2HaGWk=

That 101 Switching Protocols response is the moment the conversation changes. Before it, the connection is HTTP. After it, the same TCP connection starts carrying WebSocket frames.

This matters because WebSockets were designed for the real internet, not for a perfect lab environment.

Using HTTP for the handshake helped WebSockets pass through infrastructure that already understood web traffic: browsers, proxies, firewalls, TLS termination, and load balancers.

That is not just technical elegance.

That is survival.

A protocol that cannot pass through corporate networks, proxies, and firewalls is a protocol that will fail in boring but painful ways. WebSockets worked because they entered through the same doors HTTP already opened.

The Weird Parts Are There for a Reason

Some parts of the WebSocket protocol look strange when you first see them: the magic key, the accept header, the masking rule, and the asymmetry between browser-to-server and server-to-browser frames.

But these details are not random. They exist because WebSockets were designed for a web full of proxies, caches, gateways, old infrastructure, and untrusted browser code.

Many of the weird parts aren’t weird because the designers overengineered. They are weird because the internet was already messy.

The magic key is not authentication

One of the strangest-looking parts of the WebSocket handshake is this header:

Sec-WebSocket-Key

The client sends a random base64 value. The server takes that value, appends a fixed globally unique identifier, hashes the result with SHA-1, base64-encodes it, and sends it back as:

Sec-WebSocket-Accept

At first glance, this looks like authentication.

It is not.

The WebSocket key does not prove who the user is. It does not replace cookies, sessions, JWTs, or any real authentication mechanism.

Its job is more specific: it proves that the server understood the request as a WebSocket upgrade and intentionally accepted it.

That distinction matters because older HTTP infrastructure could cache, replay, modify, or misunderstand traffic in dangerous ways. A random per-connection key makes it much harder for some cached or accidental HTTP response to look like a valid WebSocket upgrade response.

The key is not there to identify the user.

It is there to prevent protocol confusion.

Masking is not encryption

Another strange WebSocket rule:

Every frame sent from the browser to the server must be masked.

Server-to-browser frames do not need the same masking.

That asymmetry feels odd until you understand the threat model. Browsers run untrusted JavaScript all the time. A random website can execute code in a user’s browser and open a WebSocket connection. Without masking, malicious JavaScript could carefully craft bytes that look like HTTP traffic to old proxies or confused intermediaries.

That could lead to cache poisoning or protocol confusion.

So the browser must mask outgoing WebSocket frames using a random masking key. The payload bytes on the wire become unpredictable. The server can unmask them, but an attacker running JavaScript in the browser cannot reliably control the exact bytes that intermediaries see on the network.

That is the point.

Masking is not encryption. TLS handles encryption. Masking is not authentication. Your application still needs real identity and permission checks.

Masking is a defense against browser-controlled traffic being shaped to confuse old HTTP infrastructure.

This is the kind of detail most developers never need to implement manually because libraries handle it. But understanding it changes how you see the protocol.

WebSockets were not designed only around clean APIs. They were designed around the messy reality of the web.

After the Handshake, There Are No Requests

Once the upgrade completes, WebSockets stop behaving like HTTP.

There are no routes in the normal HTTP sense. There is no request-response lifecycle unless your application invents one. There is no automatic meaning attached to a message.

Instead, data moves through frames.

A WebSocket frame can carry text, binary data, ping, pong, close signals, or fragments of a larger message.

That sounds low-level, but it explains a lot of behavior that otherwise feels like library magic.

Ping and pong frames exist because long-lived connections need liveness checks. Close frames exist because shutting down a conversation cleanly matters. Fragmentation exists because large messages may need to be split into smaller pieces. Binary frames exist because not everything should be JSON.

The browser API hides most of this from you, which is good. But at senior level, you need to remember that WebSockets are not “HTTP but faster.”

They are a different communication model running over a persistent TCP connection.

And once your system becomes connection-oriented instead of request-oriented, the production problems stop looking surprising.

Scaling WebSockets Means Scaling Connection State

Here is the most important architectural difference:

HTTP scales around requests. WebSockets scale around connections.

With stateless HTTP, a load balancer can send request one to server A, request two to server B, and request three to server C. That is fine if the servers do not hold critical in-memory state.

But with WebSockets, the client is physically connected to one running process.

That creates a chain of production questions.

Where do you store connection state? How do you know which server a user is connected to? What happens when that server restarts? How do you broadcast one event to users connected across many servers? How do you drain connections gracefully? How do you apply backpressure when clients cannot keep up?

This is where many WebSocket systems stop being “real-time features” and start becoming distributed systems.

The load balancer also becomes part of the architecture, not just plumbing. Putting a load balancer in front of normal HTTP servers is routine. Putting one in front of WebSocket servers requires more care.

The load balancer must support protocol upgrade. It must allow long-lived connections. It must have timeout settings that match your application behavior. A default idle timeout that works fine for HTTP APIs can quietly break WebSocket connections.

Then there is routing.

If your WebSocket server keeps connection-specific state in memory, sticky sessions may help keep a reconnecting client routed to the same backend. But sticky sessions are not a complete architecture.

They reduce some routing problems, but they do not solve fan-out across multiple servers. They also do not help if the original server is gone.

A better mental model is this:

The WebSocket server should own active delivery, not durable truth.

It can hold active socket connections, authenticate clients, and push messages. But durable state, business decisions, permissions, and event history should generally live elsewhere.

Otherwise, every deploy, crash, or autoscaling event risks becoming a state-loss event.

Broadcasting Is Easy Until You Have Two Servers

Broadcasting looks trivial in a single-process demo.

A message arrives. You loop through connected clients. You send the message. Done.

But now imagine you have two WebSocket servers.

User A is connected to server 1. User B is connected to server 2. User A sends a chat message. Server 1 receives it.

How does user B get it?

Server 1 cannot directly write to user B’s socket because that socket lives inside server 2.

This is where you need a shared messaging layer. Common options include Redis Pub/Sub, RabbitMQ, Kafka, NATS, or a managed cloud messaging service.

The pattern usually looks like this:

The socket is only the last mile. The broker is what lets one message reach clients connected across many servers.

The socket is only the last mile. The broker is what lets one message reach clients connected across many servers.

In prose, the flow is simple:

  1. WebSocket server receives a client message.
  2. Application validates and processes it.
  3. An event is published to a broker or stream.
  4. Other WebSocket servers receive the event.
  5. Each server forwards it to the clients connected to that server.

At that point, the WebSocket connection is just the final delivery channel. The real system is the event architecture behind it.

This is why “we’ll just use WebSockets” is often an incomplete architecture decision.

Use WebSockets for the connection. Use something else for coordination, durability, fan-out, and recovery.

Dead Connections Do Not Always Die Loudly

A WebSocket can fail without giving you a clean goodbye.

A laptop sleeps. A mobile device switches networks. A NAT mapping expires. A proxy drops an idle connection. A server crashes. A deployment kills a process. A user closes a tab without a graceful close completing.

Sometimes the client knows. Sometimes the server knows. Sometimes neither side knows immediately.

This is why serious WebSocket systems need heartbeat logic.

A heartbeat is usually a ping/pong mechanism. One side periodically asks:

Are you still there?

The other side responds:

Yes.

If a response does not arrive within a reasonable window, the connection is treated as dead and closed. The client may reconnect. The server may clean up resources.

This sounds simple, but the details matter.

Heartbeat intervals that are too aggressive create unnecessary traffic. Intervals that are too slow leave dead connections hanging around. Overly eager reconnection logic can create thundering herds when many clients reconnect at once.

A production client should usually reconnect with backoff and jitter, not with a tight loop. That is a small implementation detail until your service restarts and fifty thousand browsers try to reconnect at the same time.

Then it becomes the incident.

WebSockets Also Change Deployment Behavior

HTTP deployments can be relatively forgiving. A request finishes, the server shuts down, and new requests go elsewhere.

With WebSockets, clients may stay connected for minutes, hours, or longer.

So what happens when you deploy?

Do you kill all connections immediately? Do you drain old connections? Do you notify clients before closing? Do clients reconnect safely? Can the new server understand messages from old clients? Can old clients understand messages from the new server?

This is one of those areas where the protocol is not the hard part.

The operational behavior is.

A mature WebSocket deployment strategy often needs graceful shutdown:

  • stop accepting new connections
  • keep existing connections briefly
  • notify clients if needed
  • close connections with a proper close code
  • rely on clients to reconnect with backoff
  • make reconnect safe and idempotent

Otherwise, a normal deployment can become a user-visible outage. And if every client reconnects at the same time, the outage can become the incident.

Sometimes SSE or Polling Is the Better Architecture

Senior engineering is not knowing how to use a technology.

It is knowing when not to use it.

WebSockets are a good fit when both sides need to send unpredictable messages with low latency.

Good examples include multiplayer games, collaborative editing, chat, live cursors, presence, real-time control panels, trading interfaces, and interactive dashboards where the client also sends frequent actions.

But WebSockets are often overkill when communication is mostly one-way.

If the server only needs to push updates to the browser, Server-Sent Events may be simpler. SSE works over HTTP. It has built-in browser reconnection behavior. It is easier to inspect, easier to cache around, and often easier to operate.

For notifications, progress updates, status feeds, logs, and timelines, SSE can be enough. And if the data is only needed occasionally, normal HTTP is still the better tool.

Do not open a long-lived connection just to fetch something once. Do not build a real-time system for a feature where five-second polling would be perfectly fine.

Real-time is not free.

It moves complexity from the user experience into your infrastructure.

Sometimes that trade is worth it.

Sometimes it is theater.

The Tech Lead Question

Most teams ask the wrong first question.

They ask:

Can we use WebSockets for this?

The better question is:

What operational model are we accepting if we use WebSockets?

That question forces better design. It moves the discussion from excitement to consequences.

Capacity: How many concurrent connections do we expect? How long do they stay open? What message rate should the system handle?

Delivery: Can messages be lost? Do clients need replay? Do we need ordering? Do we need delivery acknowledgments?

Security: Do we authenticate only when the socket opens? Do we authorize every message? What happens when permissions change while a socket is already open?

Operations: What happens during deploys? What happens when one server dies? What happens when one region goes down? How do we observe connection count, message rate, latency, errors, reconnects, and dropped clients?

These are the questions that separate “we added WebSockets” from “we designed a real-time system.”

The Better Mental Model

A WebSocket is not just a faster API call.

It is a long-lived relationship between a client and a server. That relationship has a lifecycle: it opens, authenticates, sends, receives, idles, fails, reconnects, and eventually closes.

Every one of those stages has production consequences.

The protocol gives you a pipe. It does not give you a complete system.

You still need authentication, authorization, message validation, backpressure, observability, retries, fan-out, graceful shutdown, and a clear decision about what happens when delivery fails.

That is the real WebSocket lesson.

The HTTP upgrade handshake exists because the web already had infrastructure WebSockets needed to pass through. The magic key exists because protocol confusion is real. Client masking exists because browsers run untrusted code. Ping and pong exist because long-lived connections fail quietly. Message brokers become necessary because one server is not the whole system. Alternatives like SSE still matter because not every real-time feature needs full-duplex communication.

So yes, WebSockets are elegant. But their elegance is not only in sending messages both ways.

Their elegance is in how much internet history they quietly carry.

They start as HTTP because the web already had doors they needed to walk through. They use a strange key because old infrastructure can misunderstand new conversations. They mask browser frames because the browser is both a platform and a threat boundary. They need heartbeats because networks rarely fail politely. They need brokers because a socket on one server is invisible to another. They need operational discipline because a connection that lives for hours does not behave like a request that lives for milliseconds.

That is the part the demo cannot teach you.

The protocol is simple enough to demo in five minutes.

The system around it is where senior engineering begins.


메타데이터
post_id
ad79f2c24f79
slug
websockets-look-simple-until-you-have-to-run-them-in-production-ad79f2c24f79
url
https://blog.devops.dev/websockets-look-simple-until-you-have-to-run-them-in-production-ad79f2c24f79
canonical_url
https://blog.devops.dev/websockets-look-simple-until-you-have-to-run-them-in-production-ad79f2c24f79
author_url
https://medium.com/@basel.issmail
status
ok
fetched_at
2026-06-10 21:21:38