When I first learned WebSockets, the idea looked almost too simple.
HTTP works like this:
Client
│
│ request
▼
Server
│
│ response
▼
Client
WebSocket looked better for real-time systems:
Client
│
│ persistent connection
│
▼
Server
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
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
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
The browser receives those prices through one WebSocket connection.
Everything looks good.
Then the user's Wi-Fi disappears.
Browser ─────X───── Server
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)
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 ✅
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
Then the connection disappeared.
While it was gone, the server produced:
message 104
message 105
message 106
message 107
The client reconnects.
The next message arriving is:
message 108
Now the client has:
101
102
103
108
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
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?
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
closeevent immediately.
Not always.
A clean shutdown can look like this:
Client
│
│ CLOSE
▼
Server
│
│ CLOSE ACK
▼
Client
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
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
A deployment restarts the server.
All 50,000 connections disappear nearly together.
The easiest reconnection code would be something like:
socket.onclose = () => {
connect();
};
Looks reasonable.
But now:
Server restarts
↓
50,000 clients disconnect
↓
50,000 clients reconnect
↓
recovering server gets hammered
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
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);
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
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
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
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 │
└─────────────────────┘
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.
A production WebSocket system should not be designed around:
How can I keep this connection alive forever?
A better question is:
When this connection fails,
how does my system recover without
losing state, duplicating work,
or overwhelming itself?
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 ❌
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
They sound like the same thing.
They are not.
And the difference matters in production.
Top comments (0)