DEV Community

Cover image for WebSocket Engineering — Part 1: The Connection Is the Easy Part
surajrkhonde
surajrkhonde

Posted on

WebSocket Engineering — Part 1: The Connection Is the Easy Part

When I first learned WebSockets, the idea looked almost too simple.

HTTP works like this:

Client
  │
  │ request
  ▼
Server
  │
  │ response
  ▼
Client
Enter fullscreen mode Exit fullscreen mode

WebSocket looked better for real-time systems:

Client
  │
  │ persistent connection
  │
  ▼
Server
Enter fullscreen mode Exit fullscreen mode

Connect once.

Keep the connection open.

The server can push messages whenever it wants.

Perfect for:

  • chat
  • trading prices
  • notifications
  • multiplayer games
  • dashboards
  • live tracking

I thought that was the main idea.

Then I started asking a different question:

What happens when that connection breaks?

That question changed how I looked at WebSockets.


A WebSocket connection is not permanent

It is easy to draw this architecture:

Browser ─────────────── Node.js Server
          WebSocket
Enter fullscreen mode Exit fullscreen mode

And mentally imagine that line remaining there forever.

Production does not work that way.

A connection can disappear because the user moves from Wi-Fi to mobile data.

A laptop can go to sleep.

A phone can enter a lift and lose signal.

A server can restart during deployment.

A load balancer can close an idle connection.

A proxy can enforce its own timeout.

The network path itself can change.

So the real lifecycle is closer to this:

connect
   ↓
communicate
   ↓
disconnect
   ↓
recover
   ↓
reconnect
   ↓
communicate again
Enter fullscreen mode Exit fullscreen mode

That was my first important shift in thinking.

Disconnection is not an edge case in a real-time system. It is part of normal operation.


A small trading example

Imagine I am building a live trading screen.

The server is continuously sending prices:

BTC  → 117000
BTC  → 117010
BTC  → 117025
BTC  → 117040
Enter fullscreen mode Exit fullscreen mode

The browser receives those prices through one WebSocket connection.

Everything looks good.

Then the user's Wi-Fi disappears.

Browser ─────X───── Server
Enter fullscreen mode Exit fullscreen mode

Now there are already several questions.

Does the browser know immediately that the connection died?

Does the server know?

Should the browser reconnect immediately?

What if 50,000 users disconnected at the same time because the server restarted?

What prices were produced while this user was disconnected?

What if the user sent an order just before the connection disappeared?

Did the server receive it?

Should the client send it again?

What if sending it again creates two orders?

Suddenly:

new WebSocket(url)
Enter fullscreen mode Exit fullscreen mode

is the least interesting part of the system.


Reconnecting the socket is not the same as recovering the application

This distinction became very important to me.

Suppose the connection dies and five seconds later we successfully create another one.

Old connection ❌

        ↓

New connection ✅
Enter fullscreen mode Exit fullscreen mode

At the transport level, we recovered.

But did the application recover?

Not necessarily.

Imagine the client had already received:

message 101
message 102
message 103
Enter fullscreen mode Exit fullscreen mode

Then the connection disappeared.

While it was gone, the server produced:

message 104
message 105
message 106
message 107
Enter fullscreen mode Exit fullscreen mode

The client reconnects.

The next message arriving is:

message 108
Enter fullscreen mode Exit fullscreen mode

Now the client has:

101
102
103
108
Enter fullscreen mode Exit fullscreen mode

Where did 104–107 go?

The WebSocket connection is healthy again.

The application state is not.

That is the deeper problem.


There are two different recovery problems

I now find it useful to separate them.

Transport recovery

Can I establish another WebSocket connection?

connection lost
      ↓
reconnect
      ↓
connected
Enter fullscreen mode Exit fullscreen mode

State recovery

After reconnecting:

Who is this client?

Which channels was it subscribed to?

Which messages did it miss?

What was the last event it received?

Did it have unacknowledged outbound messages?

What state should be restored?
Enter fullscreen mode Exit fullscreen mode

Transport recovery is comparatively easy.

State recovery is where real system design starts.

The WebSocket reconnection guide I was studying makes exactly this distinction: reconnecting the transport is straightforward, while synchronizing client/server state after reconnect is the harder problem.


The network can fail without saying goodbye

Another assumption I had was:

If the client disconnects, surely the server gets a close event immediately.

Not always.

A clean shutdown can look like this:

Client
   │
   │ CLOSE
   ▼
Server
   │
   │ CLOSE ACK
   ▼
Client
Enter fullscreen mode Exit fullscreen mode

Both sides know the connection ended.

But imagine someone walks into a lift.

The Wi-Fi simply disappears.

There may be no nice goodbye packet.

Client
   X
   X
   X
Server
Enter fullscreen mode Exit fullscreen mode

The server might continue believing that the socket exists.

That creates another problem:

How does the server know whether a connection is actually alive?

That question eventually leads us to heartbeats, ping/pong frames, zombie connections, and proxy idle timeouts.

But there is another problem first.


What if everybody reconnects together?

Imagine a server has:

50,000 WebSocket clients
Enter fullscreen mode Exit fullscreen mode

A deployment restarts the server.

All 50,000 connections disappear nearly together.

The easiest reconnection code would be something like:

socket.onclose = () => {
  connect();
};
Enter fullscreen mode Exit fullscreen mode

Looks reasonable.

But now:

Server restarts
      ↓
50,000 clients disconnect
      ↓
50,000 clients reconnect
      ↓
recovering server gets hammered
Enter fullscreen mode Exit fullscreen mode

We fixed one failure by creating another one.

And if every client retries every second:

1 second  → 50,000 attempts
2 seconds → 50,000 attempts
3 seconds → 50,000 attempts
Enter fullscreen mode Exit fullscreen mode

the recovering server may never get enough breathing room to recover.

This is where another distributed-systems concept appears:

the thundering herd problem.

Interesting thing: this is not really a “WebSocket concept.”

It appears in caches.

Queues.

Database retries.

Distributed locks.

Service retries.

And now WebSocket reconnection.

The technology changes.

The failure pattern repeats.


This is what I like about going deeper

At first, WebSockets looked like an API:

const ws = new WebSocket(url);
Enter fullscreen mode Exit fullscreen mode

Then it became networking.

Then distributed systems.

Then reliability.

Then state management.

Then message delivery guarantees.

Then observability.

And concepts I had already seen elsewhere started appearing again:

ACK
retry
backoff
jitter
idempotency
buffering
sequence numbers
timeouts
TTL
distributed state
Enter fullscreen mode Exit fullscreen mode

For example, ACK and retry immediately reminded me of RabbitMQ.

A RabbitMQ consumer can process a message but lose the ACK.

The broker may deliver the message again.

Now duplicate processing becomes possible.

WebSockets can create almost the same uncertainty:

Client sends message
       ↓
Server receives it
       ↓
Server processes it
       ↓
ACK travelling back
       ↓
connection dies
Enter fullscreen mode Exit fullscreen mode

What does the client know?

Nothing.

Maybe the server processed it.

Maybe it didn't.

Retrying protects against message loss.

Retrying also creates duplicate risk.

And suddenly we are talking about idempotency again.

Different technology.

Same distributed-systems problem.

That is the part I find much more interesting than memorizing APIs.


My current mental model

I no longer think of WebSocket architecture as:

Client ↔ Server
Enter fullscreen mode Exit fullscreen mode

I think of it more like:

                 ┌─────────────────────┐
                 │     Connection      │
                 │      Lifecycle      │
                 └──────────┬──────────┘
                            │
                  connect / disconnect
                            │
                            ▼
                 ┌─────────────────────┐
                 │      Heartbeat      │
                 │  Is the peer alive? │
                 └──────────┬──────────┘
                            │
                            ▼
                 ┌─────────────────────┐
                 │    Reconnection     │
                 │ backoff + jitter    │
                 └──────────┬──────────┘
                            │
                            ▼
                 ┌─────────────────────┐
                 │    State Recovery   │
                 │ session / replay    │
                 └──────────┬──────────┘
                            │
                            ▼
                 ┌─────────────────────┐
                 │ Message Reliability │
                 │ ACK / retry / IDs   │
                 └──────────┬──────────┘
                            │
                            ▼
                 ┌─────────────────────┐
                 │    Observability    │
                 │ errors / close code │
                 └─────────────────────┘
Enter fullscreen mode Exit fullscreen mode

Now WebSocket feels less like:

“a persistent connection”

and more like:

a connection that I should expect to lose and know how to recover.

That is a much stronger design assumption.


The first lesson

If I had to keep only one idea from this part, it would be:

WebSocket disconnecting is normal.

Reliable recovery is the feature.
Enter fullscreen mode Exit fullscreen mode

A production WebSocket system should not be designed around:

How can I keep this connection alive forever?
Enter fullscreen mode Exit fullscreen mode

A better question is:

When this connection fails,
how does my system recover without
losing state, duplicating work,
or overwhelming itself?
Enter fullscreen mode Exit fullscreen mode

That question opens the rest of the series.


Next: Heartbeats and Zombie Connections

There is one strange failure we haven't solved yet.

Imagine:

Server thinks client is connected ✅

but

Client disappeared 5 minutes ago ❌
Enter fullscreen mode Exit fullscreen mode

No close handshake.

No useful traffic.

Just a socket that looks alive but is actually dead.

A ghost.

A zombie connection.

In Part 2, I want to go deep into why heartbeats exist and why there are actually three different layers:

WebSocket protocol ping/pong
Application-level heartbeat
TCP keepalive
Enter fullscreen mode Exit fullscreen mode

They sound like the same thing.

They are not.

And the difference matters in production.

Top comments (0)