
Imagine you're building an online customer support system. When a user sends a message, the other side needs to see it instantly. Your first instinct might be: "What if I just make the browser send a request every second asking the server for new messages?"
That's a valid starting point — but is polling every second really good enough? Is there a better way? Before WebSocket came along, front-end developers relied on these "low-tech" tricks to achieve near-real-time communication. In this article, we'll retrace that evolution from the ground up.
Picture yourself waiting for an important package, but the courier won't notify you when it arrives. You have to walk downstairs every few minutes to check your mailbox — that's short polling in a nutshell.
Client: "Any new messages?"
Server: "Nope."
Client: (waits 1s) "Any new messages?"
Server: "Yes! Here you go."
Client: (waits 1s) "Any new messages?"
...The core idea is dead simple: the client sends HTTP requests on a fixed interval, and the server returns data if there is any, or an empty response if there isn't. No special protocol needed — plain HTTP does the job.
Let's start with the smallest working version. The server randomly simulates new data arriving; the client asks every second.
Server-side (Node.js):
import http from "http";
const server = http.createServer((req, res) => {
if (req.url === "/poll") {
// Simulate: 30% chance of "new data"
const hasNewData = Math.random() < 0.3;
if (hasNewData) {
res.writeHead(200, { "Content-Type": "application/json" });
res.end(JSON.stringify({ message: "Server has new data!" }));
} else {
res.writeHead(204); // No Content
res.end();
}
}
});
server.listen(3000, () => {
console.log("Short polling server running on http://localhost:3000");
});Client-side:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Short Polling Demo</title>
</head>
<body>
<h1>Short Polling Demo</h1>
<div id="messages"></div>
<script>
function poll() {
fetch("http://localhost:3000/poll")
.then((res) => {
if (res.status === 200) return res.json();
if (res.status === 204) return null; // No new data
})
.then((data) => {
if (data?.message) {
const div = document.getElementById("messages");
div.innerHTML += ``;
}
})
.catch((err) => console.error("Poll error:", err))
.finally(() => setTimeout(poll, 1000)); // Ask again in 1 second
}
window.onload = poll;
</script>
</body>
</html>Two key details worth noting:
finally: The poll continues regardless of success or failure. A single network hiccup won't kill the polling loop.Advantages:
Disadvantages:
Here's a metaphor: you go downstairs to check your mailbox every 5 minutes. The package might arrive right after you just checked. If you switch to checking every 30 seconds, latency improves — but now you're running yourself ragged.
What if the courier could proactively notify you, instead of you running downstairs on repeat?
That's exactly what Long Polling sets out to solve.
Long polling inverts the logic: the client sends one request, and if the server has nothing new, it holds the connection open instead of replying immediately — only responding when data arrives or a timeout fires. The client then immediately issues the next request, forming a "request → wait → respond → request" loop.
Client: "Any new messages?"
Server: ... (holding, waiting for data)
Server: "Yes! Here you go."
Client: (immediately) "Any new messages?"
Server: ... (holding again)The critical difference: in short polling, the client does the waiting (via setTimeout); in long polling, the server does the waiting (by holding the connection).
Let's build a simple chat room where messages arrive "instantly" via long polling.
Server-side:
import http from "http";
let clients = []; // Pending client connections waiting for a response
let message = null; // Message to broadcast
const server = http.createServer((req, res) => {
// CORS
res.setHeader("Access-Control-Allow-Origin", "*");
res.setHeader("Access-Control-Allow-Methods", "GET, POST");
res.setHeader("Access-Control-Allow-Headers", "Content-Type");
// Long polling endpoint: clients come here to wait
if (req.url === "/poll") {
clients.push(res);
// Clean up when the client disconnects
req.on("close", () => {
clients = clients.filter((c) => c !== res);
});
}
// Send endpoint: broadcast a message to all waiting clients
if (req.url === "/send" && req.method === "POST") {
let body = "";
req.on("data", (chunk) => (body += chunk.toString()));
req.on("end", () => {
// Notify every waiting client
clients.forEach((client) => {
client.writeHead(200, { "Content-Type": "application/json" });
client.end(JSON.stringify({ message: body }));
});
clients = []; // Clear the waiting queue
res.end("OK"); // Reply to the sender
});
}
});
server.listen(3000, () => {
console.log("Long polling server running on http://localhost:3000");
});Client-side:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Long Polling Chat Room</title>
</head>
<body>
<h1>Long Polling Chat Room</h1>
<div id="messages"></div>
<input id="msgInput" type="text" placeholder="Type a message..." />
<button onclick="send()">Send</button>
<script>
// Long polling: fire the next request as soon as a response arrives
function poll() {
fetch("http://localhost:3000/poll")
.then((res) => res.json())
.then((data) => {
if (data.message) {
document.getElementById("messages").innerHTML +=
``;
}
poll(); // Immediately ask again after receiving data
})
.catch((err) => {
console.error("Poll error:", err);
setTimeout(poll, 5000); // Retry after 5s on error
});
}
function send() {
const input = document.getElementById("msgInput");
fetch("http://localhost:3000/send", {
method: "POST",
body: input.value,
headers: { "Content-Type": "text/plain" },
})
.then(() => (input.value = ""))
.catch((err) => console.error("Send error:", err));
}
window.onload = poll;
</script>
</body>
</html>A few design decisions worth highlighting:
poll() calls itself recursively inside .then(), not via setTimeout. As soon as data arrives, the next waiting cycle begins immediately — minimal latency.clients array: Each long-poll request's res object is saved. When a message arrives, the server calls end() on all of them at once. This leverages Node.js's event-driven, non-blocking model.close event cleanup: When a client disconnects (tab closed, network drop), its res must be removed from the array. Otherwise you get a memory leak.| Dimension | Short Polling | Long Polling |
|---|---|---|
| Request frequency | Fixed interval, independent of data | Data-driven, only responds when there's data |
| Latency | Up to 1 full interval | Near-real-time in theory |
| Server overhead | Many wasted requests | Holding connections; heavier with many clients |
| Implementation complexity | Trivial | Moderate — must manage a connection queue |
| Best for | Low-frequency updates (e.g., system status) | Medium real-time needs (chat, notifications) |
A natural question arises: HTTP is a stateless request-response protocol — how can the server just "hold off" on replying?
The answer is that while HTTP defines the request-response model, it does not mandate how quickly the server must respond. RFC 7230 defines no mandatory request timeout. Timeout handling is entirely up to the client and server implementations.
In the browser, fetch has no built-in timeout by default (though browsers may impose a very long one internally — Chrome's is around 300 seconds). If you need explicit timeout control, you can do this:
function fetchWithTimeout(url, options = {}, timeout = 5000) {
return Promise.race([
fetch(url, options),
new Promise((_, reject) =>
setTimeout(() => reject(new Error("Request timeout")), timeout)
),
]);
}
// Usage: error out if no response within 5 seconds
fetchWithTimeout("/api/long-poll", {}, 5000)
.then((res) => res.json())
.catch((err) => console.error("Timeout or network error:", err));This flexibility is the key. Upon receiving a request, the server can choose not to call res.end() immediately. Instead, it stashes the response object and only finalizes the response when data becomes available. The underlying TCP connection stays open throughout, and TCP itself guarantees reliable data delivery — even if the connection lingers, no packets are lost.
So long polling isn't black magic. It simply exploits two properties of HTTP:
{ timeout: true } so the client reconnects. This prevents proxy servers or load balancers from killing idle connections./poll/room1) map to different rooms, each with its own independent clients array.Keywords: Short Polling, Long Polling, HTTP 204, connection holding, real-time communication
This article traced the evolution from short polling to long polling — from "periodic page refreshes" to pseudo-real-time bidirectional communication. But short polling and long polling are just the warm-up. The next article dives into WebSocket itself — the protocol that finally delivers true full-duplex communication. We'll dissect the HTTP Upgrade handshake, decode WebSocket frames bit by bit, and build a complete chat room from scratch in Node.js — no libraries, just the raw protocol.