Have you ever used a chat application or played an online game and noticed how everything updates instantly? Messages appear right away. Movements show up in real time. Notifications pop up without you ever refreshing the page. That behavior feels completely natural to us now, but it’s not how the web originally worked.
The Problem with Traditional HTTP
To understand why WebSockets exist, we first need to understand the problem they solve. Traditionally, websites communicate with servers using a protocol called HTTP. With HTTP, the browser sends a request, the server sends back a response, and then the connection closes. That is the end of the interaction. If the browser wants more information, it has to start all over again and send another request.
Now, imagine you’re building a chat app using only HTTP. The server can’t just send a message whenever something new happens. Instead, the browser has to keep asking, “Do you have something new?” over and over again. This approach is called polling. It works, but it’s incredibly inefficient, slow, and wastes resources. Most of the time, the server just replies with “nothing new,” and both sides move on. This is where WebSockets come in.
Introducing WebSockets: A Better Way to Communicate
WebSockets change the rules of communication. Instead of opening and closing a connection for every single message, a WebSocket creates a persistent, stateful connection between the browser and the server. Once that connection is established, it stays open.
This allows both the browser and the server to send messages to each other at any time. No more constant asking.
A good way to think about this is to compare sending emails to having a phone call. HTTP is like email. You send a message, wait for a reply, and then send another message later. WebSockets are like a phone call. Once the call starts, both people can talk freely without reconnecting every time they want to speak.
How the Connection Works
When a WebSocket connection starts, it actually begins as a normal HTTP request. The browser sends a special request to the server, asking if it can upgrade the connection to a WebSocket. If the server agrees, the connection switches protocols and stays open. From that point on, both sides can exchange messages instantly with very little overhead.
Client-Side Implementation with JavaScript
Let’s look at what this looks like in the browser. To create a WebSocket connection, we can use the built-in WebSocket API in JavaScript.
Establishing a Connection
Creating a connection is a one-liner. This code, running inside a web page, initiates the process.
// Create a new WebSocket connection to our server
const ws = new WebSocket('ws://localhost:3001');
Notice that we use the ws:// protocol. This single line does a lot behind the scenes. The browser reaches out to the server at localhost on port 3001 and asks to open a WebSocket connection. If the server accepts, the connection stays open and ready for communication.
Handling the ‘open’ Event
How do we know when the connection is ready? We don’t want to send messages before the connection is open. This is where events come in. When the connection is successfully established, the browser fires an open event.
We can listen for that event like this:
ws.onopen = () => {
console.log('Connection established with server!');
};
When you see this message in the console, that’s your signal that the browser and the server are now connected. From this point on, both sides can start sending messages to each other.
Sending Data to the Server
Once the connection is open, sending a message is as simple as calling the send method on the socket object.
ws.onopen = () => {
console.log('Connection established with server!');
// Send a greeting as soon as we connect
ws.send('Hello, Server!');
};
At this point, the message is immediately sent over the open connection. There is no request and response like HTTP. The message just goes through, and the server can handle it however it wants.
Receiving Messages from the Server
Next, we need to handle messages coming from the server. This is one of the most important parts of WebSockets. Whenever the server sends data, the browser automatically triggers a message event.
We can listen for that event like this:
ws.onmessage = (event) => {
console.log('Received from server:', event.data);
};
When the server sends data through the WebSocket, this function is automatically triggered. The received message is available in event.data. The data could be plain text, JSON, or even binary data. For now, we’ll keep things simple and assume we’re working with text messages.
Handling Errors and Closed Connections
Of course, not everything always goes perfectly. Connections can fail. To handle this, we listen for the error event.
ws.onerror = (error) => {
console.error('WebSocket Error:', error);
};
Finally, when the connection closes—perhaps the user leaves the page or the server shuts down—the close event fires.
ws.onclose = () => {
console.log('Connection with server closed.');
// You could attempt to reconnect here if needed
};
Server-Side Implementation with Node.js
On the back end, WebSockets are responsible for keeping a persistent connection open. In Node.js, one of the most commonly used libraries for this is called ws. It’s lightweight, fast, and great for learning.
First, we import the library and create the server.
// Import the WebSocket library
import { WebSocketServer } from 'ws';
// Create a new WebSocket server on port 3001
const wss = new WebSocketServer({ port: 3001 });
The moment this line runs, Node.js starts listening for incoming WebSocket connections on that port. Unlike an HTTP server, there are no routes or endpoints. The server simply waits for clients to connect.
Next, we react when a client connects. The server emits a connection event for each new client.
wss.on('connection', (socket) => {
console.log('A new client has connected!');
// Listen for messages from this specific client
socket.on('message', (message) => {
const receivedMessage = message.toString();
console.log('Received:', receivedMessage);
// Immediately send a response back to the client
socket.send(`Server received your message: "${receivedMessage}"`);
});
});
When a client connects, we receive a socket object, which represents that single open connection. We then listen for message events on that socket. The message arrives as raw data, so we convert it to a string. After receiving it, the server can send a response back instantly.
Example: A Simple Real-Time Messaging App
To really understand how WebSockets work, let’s build a small but realistic example: a simple messaging app where multiple clients can send and receive messages in real time.
Server-Side Logic
The server will broadcast any message it receives to all connected clients.
import { WebSocketServer } from 'ws';
const wss = new WebSocketServer({ port: 3001 });
wss.on('connection', (socket) => {
console.log('Client connected.');
socket.on('message', (message) => {
const receivedMsg = message.toString();
console.log(`Received message => ${receivedMsg}`);
// Broadcast the message to all connected clients
wss.clients.forEach((client) => {
// Check if the client's connection is still open
if (client.readyState === client.OPEN) {
client.send(receivedMsg);
}
});
});
socket.on('close', () => {
console.log('Client disconnected.');
});
});
console.log('WebSocket server is running on port 3001');
In a real messaging app, messages aren’t just sent back to the same user; they’re broadcast to everyone else. So, instead of replying only to the socket, we loop through all connected clients (wss.clients) and send the message to each one. Now, every connected user receives the message instantly.
Client-Side Logic
This could be a browser, but to keep things simple, we’ll use a basic HTML page with JavaScript.
// Connect to the WebSocket server
const ws = new WebSocket('ws://localhost:3001');
ws.onopen = () => {
console.log('Connected to the messaging server!');
// Simulate a user sending a message
ws.send('A new user has joined the chat!');
};
ws.onmessage = (event) => {
// Display the message received from the server
console.log('New message:', event.data);
};
ws.onclose = () => {
console.log('Disconnected from the messaging server.');
};
As soon as this code runs, the message is sent to the server. The server receives it, then immediately broadcasts it to all connected clients, including this one. No refresh, no polling.
This same pattern is used in chat apps, live comment sections, multiplayer games, and collaborative tools. The only difference in bigger applications is the addition of structure, validation, and security. The important thing to understand is that WebSockets aren’t about sending one message. They’re about keeping a relationship open. Once the connection exists, both sides can talk whenever they want.