It's IPL final night. Rohan is watching the match, and his friend Ananya is watching from another room. Every time a wicket falls, Rohan finds out almost instantly — his phone buzzes with a score alert before he's even looked up. But when Rohan messages Ananya "OUT!" on an old-style chat app — the kind that only checks for new messages when you manually reload the page — Ananya doesn't see it until she happens to reopen the app three overs later. Same event, same distance apart, wildly different delay. What makes the difference between an app that tells you things the moment they happen, and one that makes you go ask? That difference is the entire subject of this chapter: how real-time chat applications actually work under the hood, and what "real-time" costs a server to deliver.
The Refresh Problem: Why Ordinary Web Pages Are a Bad Fit for Chat
To see why chat needed a different kind of technology, first look at why the ordinary way websites work doesn't suit it.
When you open a regular webpage — say, your school's result portal — your browser sends one request to a server ("give me this page"), the server sends back one response ("here's the HTML"), and that exchange is done. This request-then-response pattern is called HTTP, and it is the backbone of the web. It works beautifully for pages that don't change every second. But a chat conversation isn't like that. A new message can arrive at any unpredictable moment — your friend might reply in two seconds or in twenty minutes — and your screen needs to show it the instant it arrives, without you doing anything at all.
The catch is that in plain HTTP, the server can never contact you first. It can only reply to a request you already sent. So if your friend's message is sitting on the server, your app has no way of being told about it — unless your app keeps asking.
The Naive Fix: Polling, and What It Actually Costs
The first solution most beginners (and, historically, many early real chat systems) reach for is called polling: your app quietly sends a request every few seconds asking "anything new for me?" — the digital equivalent of repeatedly refreshing a cricket score page instead of waiting for a push notification. If there's a new message, the server sends it back. If not, it sends back an empty "nothing new" reply. Either way, the app asks again a few seconds later.
Polling works, and it's simple to build. But look closely at what it costs. Suppose two friends are chatting while doing homework together, sending a message roughly once every 150 seconds (2.5 minutes) on average, and their app polls the server every 3 seconds to check for updates. Over one hour (3,600 seconds):
Number of polling requests sent by one device in 1 hour
= 3600 seconds / 3 seconds per poll
= 1200 requests
Number of messages actually exchanged in that hour
= 3600 seconds / 150 seconds per message
= 24 messages
Out of those 1,200 requests, at most 24 of them could possibly land on a moment when a genuinely new message was waiting. The remaining 1,200 − 24 = 1,176 requests get an empty "nothing new" reply. That's 1,176 ÷ 1,200 ≈ 98% of all requests wasted — sent, received, and processed by the server, purely to be told there was nothing to report.
Now scale that up. If a school rolls this chat feature out to 1,000 students, all polling at the same 3-second interval, the server has to handle:
Total requests per hour = 1000 students x 1200 requests each
= 1,200,000 requests
...and roughly 98% of those 1,200,000 requests carry no new information at all.
Over a million requests an hour just to maintain the illusion of "live" updates, for a feature that, honestly, isn't updating all that often. This is the core tradeoff of polling: poll too slowly and messages feel delayed; poll too fast and you flood the server with mostly wasted work. There's no single interval that fixes both problems at once — you are always trading freshness against server load.
A Persistent Line: How WebSockets Actually Work
Think about two different ways of finding out whether your father has left for the railway station: you could message him every three minutes asking "left yet?" — or you could just stay on a phone call with him, so that the moment he leaves, he simply tells you, with nobody needing to ask first. The second option is a real-time connection: a channel that stays open, so either side can speak the instant it has something to say.
That second approach is exactly what a technology called WebSocket gives to web applications. Instead of your browser opening a brand-new, short-lived connection for every single check, a WebSocket connection is opened once and then kept open for as long as the chat window stays active. Both your browser and the server can send data over it at any moment, in either direction, without either side asking first. When your friend's message arrives at the server, the server can push it straight down your already-open connection immediately — there is no polling interval to wait out.
A WebSocket connection begins life as a regular HTTP request that politely asks the server to change the rules of the conversation. This is called the WebSocket handshake. A real handshake, with the headers it actually requires, looks like this:
Client sends:
GET /chat HTTP/1.1
Host: chat.example.com
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
Sec-WebSocket-Version: 13
Server replies:
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=
Two details here matter and are easy to skip past. First, Sec-WebSocket-Key is a random value the client makes up just for this one handshake. Second, the server doesn't simply echo that key back — it combines it with a fixed constant string, runs the result through a hashing function, and sends back the computed value as Sec-WebSocket-Accept. This proves the server genuinely understood it was handling a WebSocket upgrade request, and isn't, say, an old server or an in-between cache that has no idea what WebSockets are. Without both of these headers present and correctly matched, a real server rejects the request with an ordinary "400 Bad Request" — it does not upgrade the connection. Only once the server replies with status 101 Switching Protocols is the one-time HTTP exchange over, and from that moment the same underlying connection is reused as a free-flowing, two-way channel: no more requesting, just sending, in either direction, whenever there is something to say.
Building a Two-Person Chat: Client and Server Code, Traced Step by Step
Let's build the smallest possible real-time chat and trace it exactly, line by line, so there is no ambiguity about what happens and when. Here is the browser-side (client) code, using the WebSocket object that is built directly into JavaScript:
const socket = new WebSocket("wss://chat.example.com/room42");
function sendMessage(text) {
displayMessage("You", text); // show it on your own screen right away
socket.send(text); // then actually transmit it to the server
}
socket.onmessage = function(event) {
displayMessage("Friend", event.data);
};
And here is a simplified version of the server, written in the style of a Node.js WebSocket server:
const clients = []; // every connected socket sits in this list
server.on("connection", function(socket) {
clients.push(socket);
socket.on("message", function(text) {
for (const peer of clients) {
if (peer !== socket) {
peer.send(text);
}
}
});
});
Now suppose Aarav and Priya are both connected to room42 — so both of their socket objects are sitting inside the server's clients array — and Aarav types "yo" and taps send. Here is exactly what happens, in order:
sendMessage("yo")starts running on Aarav's browser.- The first line inside it,
displayMessage("You", "yo"), runs immediately. Aarav seesYou: yoappear on his own screen right away. This is called local echo — his own device shows his own message without the network being involved at all, which is exactly why it feels instant. - The next line,
socket.send("yo"), transmits the text "yo" over Aarav's already-open WebSocket connection to the server. - On the server, this arrival triggers the
"message"event handler that was attached to Aarav's socket object, withtextset to"yo". - The server loops through every socket currently in
clients. For each one, it checkspeer !== socket— "is this connected person someone other than whoever just sent the message?" When the loop reaches Aarav's own socket,peerandsocketrefer to the exact same object, sopeer !== socketis false, and Aarav is skipped. The server does not send his message back to him — correctly so, since he already saw it via local echo in step 2, and sending it back would make it appear twice. - When the loop reaches Priya's socket,
peer !== socketis true — her socket is a different object from Aarav's — sopeer.send("yo")fires, pushing the text down Priya's open connection. - This delivery triggers Priya's
socket.onmessagehandler on her browser, withevent.dataequal to"yo". displayMessage("Friend", "yo")runs on Priya's screen, andFriend: yoappears there.
Notice what made this exchange fast: nowhere in these eight steps did anyone have to ask whether a new message existed. The server acted the moment a message arrived, and it pushed data out rather than waiting to be polled. Also notice how differently the two copies of the message reached their screens: Aarav's copy reached his screen purely through local code running in his own browser (step 2), while Priya's copy reached her screen only after travelling over the network and passing through the server's relay logic (steps 3 through 8). Two completely different paths, both ending in the same word appearing on two screens within milliseconds of each other.
From Two People to Two Thousand: How Chat Servers Scale Broadcasting
The server code above uses a loop — for (const peer of clients) — to broadcast one message out to everyone else connected. That loop performs real, measurable work, and it's worth being precise about how that work grows as a room fills up.
Imagine a class announcement room with 40 students connected. When one student sends a message, the server's loop runs once for every entry in clients, checking the peer !== socket condition each time, and actually calls .send() on 39 of those 40 sockets — every socket except the sender's own. Now imagine the same room scaled up to a school-wide event with 2,000 students connected. One message now triggers 1,999 sends instead of 39.
It's tempting to say "2,000 students is 50 times more than 40 students, so the server does exactly 50 times more work per message" — but check the actual numbers: 2,000 ÷ 40 is exactly 50, while 1,999 ÷ 39 works out to about 51.3, not a clean 50. The mismatch is small but real, and it comes from that "minus one": you never send a message back to its own sender, so the send-count for a room is always listeners minus one, not simply listeners. For a small room that minus-one barely changes anything (39 versus 40 is a tiny gap), but the honest way to describe the general pattern is: broadcasting to a room of n connected people costs roughly n − 1 send operations per message — proportional to room size, but never precisely equal to it. This is exactly why very large audiences — think of a live comment section under a cricket match with lakhs of simultaneous viewers — cannot rely on this simple "loop over every connection" approach without further engineering; the broadcasting work grows in direct proportion to the audience, and truly massive systems need smarter fan-out strategies, which belong in a more advanced chapter.
Two Misconceptions Worth Correcting Now
Misconception 1: "Real-time chat apps are just refreshing really, really fast." This feels intuitive, because both polling and WebSockets can eventually get a message to your screen quickly if the numbers are tuned right. But the two are fundamentally different mechanisms. Polling repeatedly opens brand-new request-response exchanges and throws almost all of them away, as the 98%-waste example showed. A WebSocket opens one connection and reuses it indefinitely, with the server actively pushing data the instant it exists — there is no "checking" happening on the client's side at all. A chat app that "refreshes every 100 milliseconds" is not doing real-time messaging; it is just extremely fast, extremely wasteful polling, and it would still lose to a WebSocket's near-zero delay while consuming a large multiple of the server load.
Misconception 2: "The server automatically sends my own message back to me too, since I'm connected." As the eight-step trace above showed, this is exactly backwards, and it is a mistake that catches real beginner projects. A WebSocket server does nothing automatically — every rule about who receives a given message has to be written deliberately, including the peer !== socket check that specifically excludes the sender. If a student removed that check while experimenting, Aarav's own "yo" would boomerang straight back to his own screen from the server, arriving as a confusing duplicate right next to the one local echo already put there. Deciding the audience for a broadcast — who should and should not receive a particular message — is a design decision the programmer makes explicitly in code; it is never a side effect of how WebSockets happen to work.
Comparing the Two Approaches
Check Your Understanding
Q1. A school rolls this chat feature out to 500 students. If every student's device polls the server every 3 seconds, how many total polling requests will the server receive in one hour? How does that compare to polling every 5 seconds instead?
Answer: At a 3-second interval, each device sends 3600 ÷ 3 = 1,200 requests per hour, so 500 students together generate 500 × 1,200 = 600,000 requests. At a 5-second interval, each device sends 3600 ÷ 5 = 720 requests per hour, giving 500 × 720 = 360,000 requests. Polling every 3 seconds produces noticeably more server load (600,000 versus 360,000 requests) in exchange for messages arriving, at worst, only 2 seconds sooner — a poor trade compared to a WebSocket, which needs none of these repeated requests at all.
Q2. In the server code above, suppose the condition inside the loop were changed from if (peer !== socket) to nothing at all — meaning peer.send(text) runs unconditionally for every socket in clients, including the sender's own. After Aarav sends "yo", what would appear on his screen, and why?
Answer: "yo" would appear twice on Aarav's screen. The first appearance still comes from local echo — displayMessage("You", "yo") inside sendMessage() still runs regardless of anything on the server. The second appearance happens because the server, no longer skipping the sender, also calls peer.send("yo") on Aarav's own socket; this triggers his own socket.onmessage handler, which calls displayMessage("Friend", "yo"), adding a second, duplicate line.
Q3. Why can't a WebSocket server just skip the Sec-WebSocket-Key / Sec-WebSocket-Accept exchange and jump straight to sending chat data?
Answer: Because the connection begins as an ordinary HTTP request, and those two headers are how the server proves it correctly understood and deliberately agreed to upgrade a genuine WebSocket request — rather than, say, a browser or an in-between proxy misreading an unusual request. A server that receives a request missing these headers (or with a mismatched key/accept pair) is required to reject it with 400 Bad Request rather than upgrading it with 101 Switching Protocols.
Summary
Ordinary web pages use a request-then-response pattern (HTTP) where the server can never speak first — it can only answer a question you already asked. Chat needs the opposite: a way for the server to tell you the instant something happens. Polling fakes this by asking repeatedly, but the numeric example showed the real cost — about 98% of polling requests return nothing useful, and scaling that pattern to hundreds or thousands of users multiplies wasted requests into the hundreds of thousands or millions per hour. A WebSocket solves the underlying problem properly: after a one-time handshake — which genuinely requires the client's Sec-WebSocket-Key and the server's matching Sec-WebSocket-Accept, without which the server must refuse the upgrade — the two sides share one connection that stays open, letting either side push data the moment it exists, with zero repeated requests. Building a working two-person chat showed that a server does nothing automatically: the programmer must explicitly write the loop that broadcasts a message to other connected clients, explicitly exclude the sender's own socket from that broadcast, and explicitly add local echo on the client so the sender sees their own message too — skip any one of these and the chat either shows nothing, shows duplicates, or silently fails. Finally, that same broadcast loop's cost scales with room size — roughly n − 1 send operations for n connected users, not a perfectly round multiple — which is exactly why very large real-time audiences need smarter delivery strategies than a single server looping over every open connection.
Practice Exercises
Now it is time to practice! Complete these challenges to solidify your understanding:
- Exercise 1: Write a short program that demonstrates the core concept from this chapter. Test it with at least 3 different inputs.
- Exercise 2: Find a real-world example where building real-time chat applications is used in an Indian company (like TCS, Infosys, Flipkart, or ISRO). Write a paragraph explaining the connection.
- Exercise 3: Create a mind-map connecting building real-time chat applications to at least 3 other topics you have studied.