Imagine you're using a chat application.
You send:
"Hey, are you free?"
And almost instantly, the other person sees it.
There is no page refresh.
You don't click a button to check for new messages.
The message simply appears.
Now think about a few other applications:
- A stock-trading dashboard where prices continuously change
- A multiplayer game where player movements need to be synchronized
- A delivery app where you can watch a driver move on a map
- A collaborative editor where another user's changes appear immediately
- A notification system that pushes updates as soon as something happens
These applications have something important in common:
The server needs to communicate with the client without waiting for the client to make another request.
This is where WebSockets become useful.
What Are WebSockets?
WebSockets provide a persistent, two-way communication channel between a client and a server.
With traditional HTTP, communication usually looks like this:
Client
|
| HTTP Request
↓
Server
|
| HTTP Response
↓
Client
The client makes a request, and the server sends a response.
With WebSockets, the relationship changes:
Client ←────────────────→ Server
Persistent
Connection
Once the connection is established, both sides can send messages whenever they need to.
The server doesn't have to wait for the client to make another HTTP request.
That's the fundamental reason WebSockets are useful for real-time applications.
Why Isn't Regular HTTP Enough?
Suppose you're building a chat application using traditional HTTP.
The client could periodically ask:
GET /messages
The server might respond:
[
{
"from": "Alex",
"message": "Hello"
}
]
But what happens when a new message arrives?
The client doesn't automatically know.
So it has to ask again:
Client → "Any new messages?"
Server → "No"
Client → "Any new messages?"
Server → "No"
Client → "Any new messages?"
Server → "Yes"
This approach is called polling.
It works, but it has an obvious inefficiency:
The client keeps making requests even when nothing has changed.
Polling vs WebSockets
With polling:
Client → Request
Server → Response
Client → Request
Server → Response
Client → Request
Server → Response
The client repeatedly asks:
"Is there anything new?"
With WebSockets:
Client ←────────────────→ Server
Persistent
Connection
Once the connection is established, the server can push an update immediately.
For a chat application:
User sends message
↓
Server receives it
↓
Server processes it
↓
Server pushes message
↓
Connected clients receive it
This avoids repeatedly asking the server whether something has changed.
How Does a WebSocket Connection Start?
A WebSocket connection doesn't simply appear.
It begins with a handshake.
Conceptually:
Client
|
| Connection Request
↓
Server
|
| WebSocket Upgrade
↓
Client
|
| Persistent Connection
↓
Server
The initial handshake uses HTTP.
If the server accepts the upgrade, the connection switches to the WebSocket protocol.
After that, communication happens over the established WebSocket connection.
So the flow changes from:
HTTP
Request → Response
Request → Response
Request → Response
to:
WebSocket
Client ←────────────────→ Server
Now either side can send messages.
The WebSocket Lifecycle
A useful way to understand WebSockets is to think about their lifecycle:
CONNECTING
↓
OPEN
↓
MESSAGE EXCHANGE
↓
CLOSING
↓
CLOSED
1. Connecting
The client is attempting to establish the connection.
2. Open
The handshake succeeds and the connection becomes active.
3. Message Exchange
Both client and server can send messages.
Client → Server
Server → Client
Client → Server
Server → Client
This can continue as long as the connection remains open.
4. Closing
Either side can initiate the closing process.
5. Closed
The connection no longer exists.
If real-time communication is still required, the application may need to establish another connection.
Sending Messages with JavaScript
The browser provides a built-in WebSocket API.
A simple client could look like this:
const socket = new WebSocket(
"wss://example.com/socket"
);
socket.onopen = () => {
socket.send(
JSON.stringify({
type: "CHAT_MESSAGE",
message: "Hello!"
})
);
};
socket.onmessage = (event) => {
console.log("Received:", event.data);
};
socket.onerror = (error) => {
console.error("WebSocket error:", error);
};
socket.onclose = () => {
console.log("Disconnected");
};
The important events are:
onopen
The connection has been established.
onmessage
A message has been received from the server.
onerror
Something went wrong with the connection.
onclose
The connection has been closed.
The API is relatively simple.
Scaling and managing the connection is where things become interesting.
The Real Challenge: Connection Management
Opening one WebSocket connection is easy.
Managing thousands or millions of them is a completely different problem.
Imagine:
1,000 connected users
That's manageable for many systems.
Now imagine:
100,000 concurrent connections
Or:
1,000,000 concurrent connections
Suddenly, your architecture needs to consider:
- Connection limits
- Memory usage
- CPU usage
- Network bandwidth
- Authentication
- Heartbeats
- Reconnection
- Load balancing
- Connection cleanup
- Message routing
- Failure recovery
Unlike a short HTTP request, a WebSocket connection can remain open for a long time.
That changes how infrastructure needs to be designed.
WebSockets and Load Balancing
This connects directly to what we discussed on Day 3.
With a traditional HTTP application, requests can often be distributed like this:
Load Balancer
/ | \
↓ ↓ ↓
Server A Server B Server C
A request might go to Server A, while the next request goes to Server B.
With WebSockets, things are different.
Suppose Alice connects to Server A:
Alice
↓
Server A
The WebSocket connection stays attached to Server A.
Now Bob connects to Server B:
Bob
↓
Server B
What happens when Alice sends a message to Bob?
Server A knows about Alice's connection.
But Bob's connection exists on Server B.
Server A needs a way to communicate with Server B.
This is where distributed messaging becomes important.
Scaling WebSocket Servers
A common architecture can look like this:
Clients
/ | \
↓ ↓ ↓
┌──────────────────────┐
│ Load Balancer │
└──────────────────────┘
/ | \
↓ ↓ ↓
WS 1 WS 2 WS 3
\ | /
\ | /
↓ ↓ ↓
Message Broker
The responsibilities are now separated.
WebSocket servers
Maintain client connections.
Message broker
Allows WebSocket servers to exchange events.
Backend services
Handle application logic.
Database
Stores persistent data.
For example:
Alice
↓
WebSocket Server 1
↓
Message Broker
↓
WebSocket Server 2
↓
Bob
The WebSocket connection remains local to its server, while the messaging layer allows the servers to communicate.
Technologies such as Redis Pub/Sub, Kafka, or other messaging systems can be used depending on the requirements and architecture.
Broadcasting Messages
Now consider a chat room with ten users.
Alice sends:
"Hello everyone!"
The server may need to broadcast that message to the other users:
Message
↓
Server
/ / / | \ \ \
↓ ↓ ↓ ↓ ↓ ↓ ↓
U1 U2 U3 U4 U5 U6 U7
For a small room, this is straightforward.
But imagine a system with:
1 million connected users
and thousands of messages arriving every second.
Broadcasting every message to every connected user would be extremely expensive.
This is why large real-time systems need efficient message routing.
Rooms and Channels
One common solution is to organize connections into logical groups.
For example:
Room: cricket-match-123
Users:
Alice
Bob
Charlie
David
When a match update occurs:
Match Update
↓
Room: cricket-match-123
↓
Connected users in that room
A chat application might use:
Room: conversation-42
Only the participants in that conversation need to receive its messages.
This is much more efficient than broadcasting every event to every connected client.
Authentication Still Matters
A WebSocket connection should not automatically be trusted simply because it was established.
The server still needs to know:
Who is connecting, and what are they allowed to access?
A simplified flow:
Client
↓
WebSocket Connection
↓
Authentication
↓
Valid?
├── Yes → Accept
└── No → Reject
After authentication, the server can associate the connection with a user:
Connection
↓
User ID: 123
↓
Rooms:
- conversation-42
- notifications-123
This information can then be used when routing messages.
Authentication and authorization become especially important when a connection can remain open for minutes or hours.
What Happens When the Network Fails?
Real networks are unreliable.
A user might:
- Lose Wi-Fi
- Switch from Wi-Fi to mobile data
- Put their laptop to sleep
- Close the browser
- Lose VPN connectivity
- Move between networks
The server may not immediately know what happened.
This creates the problem of dead connections.
That's why WebSocket systems commonly use heartbeats.
Heartbeats and Ping/Pong
A server can periodically check whether a connection is still alive.
Conceptually:
Server → Ping
Client → Pong
If the client stops responding:
Ping
↓
No response
↓
Connection considered unhealthy
↓
Close connection
This allows the server to clean up stale connections instead of keeping resources allocated forever.
Reconnection
Disconnecting is normal.
The important question is:
What does the application do after the connection is lost?
A simple client might follow:
Connected
↓
Connection Lost
↓
Wait
↓
Reconnect
↓
Connected
But immediately reconnecting can create another problem.
Imagine a server crashes while 100,000 clients are connected.
If all 100,000 clients reconnect immediately:
100,000 clients
↓
100,000 reconnect attempts
↓
Server overload
The recovery process itself can cause another outage.
This is why real systems commonly use reconnection backoff.
For example:
Attempt 1 → Immediately
Attempt 2 → 1 second
Attempt 3 → 2 seconds
Attempt 4 → 4 seconds
Random jitter can also be added so clients don't all reconnect at exactly the same time.
What Happens to Messages During a Disconnect?
Here's a more difficult question.
Suppose Alice loses her connection.
While she's offline, the server receives:
Message A
Message B
Message C
When Alice reconnects, should she receive those messages?
If the answer is yes, the system needs a way to determine what she missed.
One approach is to assign sequence numbers:
101 → Message A
102 → Message B
103 → Message C
When Alice reconnects, she can tell the server:
"The last message I received was 101."
The server can then provide:
102
103
This turns a simple WebSocket connection into a more reliable event-delivery system.
And this is an important distinction:
WebSockets provide a communication channel. They don't automatically provide durable message delivery.
If messages must survive disconnections, your system needs persistence and recovery logic.
WebSockets vs Polling
| Polling | WebSockets | |
|---|---|---|
| Connection | Repeated HTTP requests | Persistent connection |
| Server push | Not directly | Yes |
| Real-time experience | Limited | Excellent |
| Implementation | Simpler | More complex |
| Connection management | Relatively simple | Important |
| Scaling | Usually simpler | Requires more planning |
| Best for | Infrequent updates | Frequent real-time updates |
Polling isn't inherently bad.
If data changes only occasionally, polling every few seconds may be completely reasonable.
WebSockets become more attractive when the application needs frequent, low-latency, two-way communication.
WebSockets vs Server-Sent Events
WebSockets aren't the only way to build real-time experiences.
Another option is Server-Sent Events (SSE).
With SSE:
Server
↓
↓
↓
Client
The server can continuously send updates to the client.
With WebSockets:
Client
↕
Server
Both sides can communicate.
So:
WebSockets
Good when you need two-way communication.
Examples:
- Chat
- Multiplayer games
- Collaborative applications
SSE
Useful when communication is primarily server → client.
Examples:
- Live notifications
- Streaming status updates
- Real-time dashboards
The right choice depends on the communication pattern.
When Should You Not Use WebSockets?
WebSockets are powerful, but they aren't required for every application.
Suppose your application mostly does:
GET /products
GET /users/123
POST /orders
PUT /profile
DELETE /comment
Traditional HTTP is probably enough.
You don't need a persistent connection simply because the application is modern.
Ask one question:
Does the server need to push updates to the client without waiting for another request?
If not, standard HTTP may be simpler and more appropriate.
A Production-Style WebSocket Architecture
Putting the concepts together:
Clients
/ | \
↓ ↓ ↓
┌─────────────────────┐
│ Load Balancer │
└──────────┬──────────┘
↓
┌───────────┼───────────┐
↓ ↓ ↓
WS Server WS Server WS Server
│ │ │
└───────────┼───────────┘
↓
Message Broker
↓
Backend Services
↓
Database
Each component has a responsibility:
Load Balancer
Distributes incoming connections.
WebSocket Servers
Maintain client connections.
Message Broker
Allows servers to exchange events.
Backend Services
Handle business logic.
Database
Stores persistent data.
This separation makes it possible to scale different parts of the system independently.
Common WebSocket Mistakes
1. Treating WebSockets like normal HTTP requests
A WebSocket connection remains open, so connection lifecycle and resource usage matter.
2. Ignoring disconnections
Clients will disconnect.
Your system needs a recovery strategy.
3. Broadcasting everything
Sending every event to every client doesn't scale.
Route messages only where they are needed.
4. Forgetting authentication
A persistent connection still needs authentication and authorization.
5. No heartbeat mechanism
Dead connections can consume resources if they're never detected.
6. Reconnecting too aggressively
A large number of simultaneous reconnection attempts can overload a recovering server.
7. Assuming messages are automatically durable
A WebSocket connection is not a message queue or database.
If messages need to survive disconnects, persistence and replay mechanisms are required.
The Bigger System Design Lesson
WebSockets aren't simply:
"A faster version of HTTP."
They solve a different communication problem.
HTTP works well when your application follows:
Client
↓
Request
↓
Server
↓
Response
WebSockets become useful when you need:
Client
↕
Persistent Connection
↕
Server
The basic protocol is relatively straightforward.
The difficult part is everything around it:
- Authentication
- Connection management
- Heartbeats
- Reconnection
- Message routing
- Load balancing
- Scaling
- Failure recovery
- Message persistence
That's where system design comes in.
Final Takeaway
A WebSocket gives your application a persistent communication channel.
But real-time doesn't automatically mean reliable or scalable.
At small scale, you might only need:
Client
↕
WebSocket Server
At larger scale, the architecture may evolve into:
Clients
↓
Load Balancer
↓
WebSocket Servers
↓
Message Broker
↓
Backend Services
↓
Database
And then you have to think about what happens when:
- A server crashes
- A client disconnects
- Thousands of clients reconnect
- Messages are missed
- A WebSocket server needs to communicate with another server
- Traffic suddenly increases
That's the real lesson from WebSockets:
Building a real-time connection is easy. Building a real-time system that remains reliable as connections, traffic, and failures increase is the real engineering challenge.
Top comments (0)