Are We There Yet? How Servers Learnt to Stop Nagging

I was ordering food last week and watching the rider’s little icon move across the map. I wasn’t refreshing anything. It just kept moving.
I already knew the answer people give for this: WebSockets. Notifications, rider tracking, live scores, everyone just says “it’s WebSockets” and moves on. I had said it myself a few times.
But if you had asked me how it actually works, I couldn’t have told you. How does a server push something to my phone? What did apps do before WebSockets? And what happens when a million people are watching a million riders at the same time?
So I started digging. I wanted to learn webhooks, but I ended up going through four other things first, and each one only made sense because of the one before it. By the time I got to webhooks, I understood why they exist, which I think matters more than knowing how to use them.
If you’re not from a tech background, stay with me. I’ll explain everything with simple examples.
Step 1: Just keep asking
The most basic way for an app to get fresh data is to ask for it. Every few seconds, the app asks the server, “Anything new?”
This is called short polling. Think of a kid in the back seat asking “Are we there yet?” every five seconds. It’s simple, and it works.
The problem shows up when you picture a million kids in a million cars. Almost every answer is “no.” Each question still costs the server bandwidth and effort, so most of the work is wasted.
Step 2: Wait until there’s news
So the parent says, “Stop asking. I’ll tell you when we reach.”
That’s long polling. The app asks, but the server doesn’t reply right away. It holds the question open until something actually happens (or a timeout hits). Once it replies, the app asks again.
This cuts down the useless answers a lot. But now the server is holding thousands of open questions, and the app has to send a new request after every single message. It’s better, but it still feels like a workaround.
Step 3: WebSockets
Polling is like sending letters back and forth. A WebSocket is more like a phone call. You connect once, the line stays open, and both sides can talk whenever they want. The technical term for this is full duplex. It’s why chat apps, online games, live trading screens and that moving rider icon feel instant.
I built a small WebSocket server myself, and Piyush Garg’s masterclass helped me a lot here. Reading about it is one thing, but watching a message go through a server you wrote yourself is completely different.
While building it, one thought kept bothering me. This server stays on all the time and talks to a huge number of users at once. Isn’t that one of the heaviest ways to use resources? I assumed it must be working non-stop just to keep everyone updated.
It took me a while to understand that the heavy part isn’t the talking. Most of the time, a connection just sits there quietly. The cost is in keeping every line open, because the server holds each user in memory even when nothing is happening. So the real question became: what happens when a lot of people connect?
Why WebSockets are hard to scale
A normal request is simple. The server answers and forgets you. A WebSocket server can’t forget you, because the line is still open.
Now picture a million riders and a million customers, all with open lines at the same time.
The obvious fix is a bigger machine, which is called vertical scaling. It works for a while, but one machine can only hold so many connections before it runs out of memory, CPU or network. Bigger machines also get expensive quickly, and if that one server goes down, every user is disconnected together. You pay more and take on more risk.
The other fix is more machines, called horizontal scaling. This is the sensible one, but it has a catch. A user’s connection lives on exactly one server.
Say Alice is connected to Server A and Bob is connected to Server B. Alice sends Bob a message. Server A has no idea Bob exists, because he’s sitting on a different machine. The message goes nowhere.
So adding servers didn’t fix it. It just gave me a new problem: servers that can’t reach each other’s users.

The relay server
The fix is to put a middleman between all the servers. Usually this is Redis, using something called pub/sub (publish and subscribe).
Think of the notice board in a hostel mess. Every WebSocket server subscribes to the board. When Alice’s message reaches Server A, it publishes the message to the board. Every other server sees it, and Server B, who actually has Bob, delivers it to him.
The servers don’t need to know where anyone is connected. They only talk to the relay.

In code, the idea looks roughly like this (Node.js with the ws and ioredis libraries):
import { WebSocketServer, WebSocket } from "ws"; import Redis from "ioredis"; const wss = new WebSocketServer({ port: 8080 }); const pub = new Redis(); const sub = new Redis(); // a subscribing connection can't also publish sub.subscribe("chat"); // A user on THIS server sends a message -> publish it to the relay wss.on("connection", (socket) => { socket.on("message", (data) => pub.publish("chat", data.toString())); }); // The relay delivers a message from ANY server -> send to our own users sub.on("message", (_channel, message) => { wss.clients.forEach((client) => { if (client.readyState === WebSocket.OPEN) client.send(message); }); });
The server never sends a message straight to another user. It hands everything to the relay and delivers whatever comes back to its own users.
Here’s where the numbers got interesting for me. Say one WebSocket server can handle around 10,000 users. With the relay in the middle, I can keep adding servers, and each one just connects to the relay as a client. So 10,000 servers could, in theory, handle 10,000 × 10,000, which is 100 million users.
I should be honest that this is the theory, not a guarantee. Now everything passes through the relay, so the relay itself can become the next bottleneck. Real systems deal with this by splitting messages into channels or rooms, so each server only receives what its own users need. So the relay fixed one problem and gave me another.
Step 4: Server-Sent Events
While reading all this, I came across something I had barely heard of: Server-Sent Events, or SSE.
If a WebSocket is a phone call, SSE is more like a radio broadcast. The server keeps one line open and keeps sending updates, and the app just listens. It can’t talk back on that line.
My first thought was, “so it’s just a weaker WebSocket?” But then I realised how many features only need updates going one way:
- Live scores and news feeds
- Notifications
- Progress bars for long tasks
- AI replies that appear word by word
SSE works over normal HTTP, reconnects on its own if the connection drops, and is much lighter to run than WebSockets.
So why do so few people use it? I think a big reason is habit. Most people reach for WebSockets by default. But SSE does have a real limit. If you need proper two-way communication, like in chat or games, only WebSockets give you that full duplex connection. And that is exactly what makes them expensive.
So the question isn’t really “which one is better?” It’s “do I actually need two-way communication?”
Step 5: Webhooks
Everything so far was about a server talking to a user’s device. But what about when one server needs to tell another server that something happened?
Take a payment. A customer pays through a payment provider, and your backend needs to know. You can’t keep asking them every second forever, and you can’t keep a WebSocket open to every service you use.
Webhooks flip this around. Instead of you calling them, they call you. You give the other service a URL. When something happens, it sends a request to that URL. There’s no waiting and no open connection, just one message when there’s news.
That’s when it all clicked for me. Webhooks aren’t some random concept. They’re the next step in the same chain, and they fit server-to-server communication without the cost of keeping a connection open.
What I took away
Before this, I thought “real-time” was a single feature you turn on. Now I see it more like a set of trade-offs:
- Short polling: simple, but wasteful.
- Long polling: smarter, but a bit clumsy.
- WebSockets: two-way and instant, but expensive and hard to scale.
- SSE: light and simple, but one-way only.
- Webhooks: great for server-to-server, but not a live connection to a user.
None of them is the best. Each one is the right answer for one specific problem, and each one creates the next problem.
For me, the most useful part wasn’t learning how each one works. It was understanding why each one fell short. If you keep asking, you waste effort. If you keep the line open, you pay for it. If you add servers, they stop hearing each other.
So next time someone says “it’s WebSockets,” I’ll know that’s only the middle of the story.
Where I learnt this
- Svix blog: It gave me the path from polling to webhooks.
- Piyush Garg’s masterclass: It helped me understand and implement the WebSocket server.
Originally published on Medium.