Kaneki Logo

Ilyass Ezzam

(KanekiEzz)AI Engineer & Full Stack Developer

← Back to posts
Web DevelopmentNetworkingJavaScript

Client–Server Communication Patterns: Polling, Long Polling, SSE, and WebSockets

28 April 2026·7 min read
Client–Server Communication Patterns

Most web apps need to know when something changes on the server — a new message, a new bid, a live score. HTTP was originally built around a simple request/response cycle, so over time a handful of patterns emerged to work around that limitation and get data flowing in something closer to real time.

All of these patterns fall under one umbrella: asynchronous communication. The client sends a request and doesn't block waiting for an immediate reply — the response can arrive later, on its own schedule.

The Four Patterns at a Glance

  • HTTP — The classic request/response model. The client asks, the server replies, and that's it.
  • Polling — Client-initiated, pull-based. The client just keeps asking.
  • Long Polling — Client-initiated, but the server delays the reply until it actually has something to send.
  • Server-Sent Events (SSE) — Server push, one-way. The server streams data down; the client just listens.
  • WebSockets — Full-duplex, real-time. Client and server can both send messages at any time, over one open connection.

0. HTTP: The Foundation

Before any of these patterns make sense, it helps to remember what they're all working around: plain HTTP. HTTP is a request/response protocol — the client opens a connection, sends a request, and waits for the server to send back a reply. Nothing happens unless the client asks first, which is exactly why real-time features need a workaround.

HTTP

Under the hood, HTTP runs on top of TCP, using port 80 for plain connections and port 443 once TLS is involved (HTTPS). Each exchange is self-contained: the client says what it wants, the server answers, and that exchange is done.

What's Inside a Request

A few pieces make up every HTTP request:

  • A URL — which resource the client is asking about, e.g. /api/v1/users.
  • A method — what the client wants to do with that resource. The common ones: GET to read something, POST to create something, PUT to replace something, DELETE to remove something.
  • Headers — metadata about the request, like what format the client accepts (Accept: application/json) or an auth token (Authorization: Bearer ...).
  • A body — the actual payload, used with methods like POST or PUT to send data such as a JSON object.

Here's what creating a new user might look like on the wire:


POST /api/v1/users HTTP/1.1
Host: example.com
Content-Type: application/json
Authorization: Bearer <token>
 
{
    "name": "Alice",
    "email": "alice@example.com"
}

And the server's reply, confirming the resource was created:


HTTP/1.1 201 Created
Content-Type: application/json
 
{
    "id": "1234567890",
    "name": "Alice",
    "email": "alice@example.com",
    "createdAt": "2026-07-04T10:15:30Z"
}

That's the whole model: one request, one response, connection closed or reused for the next unrelated request. It's simple and predictable, but it means the client only ever finds out about something new if it asks — the server has no way to reach out on its own. That single limitation is the reason Polling, Long Polling, SSE, and WebSockets all exist.

The Four Patterns at a Glance

  • Polling — Client-initiated, pull-based. The client just keeps asking.
  • Long Polling — Client-initiated, but the server delays the reply until it actually has something to send.
  • Server-Sent Events (SSE) — Server push, one-way. The server streams data down; the client just listens.
  • WebSockets — Full-duplex, real-time. Client and server can both send messages at any time, over one open connection.

1. Polling

Polling is the simplest approach: the client repeatedly asks the server "anything new?" on a fixed interval, regardless of whether there's actually anything new to report.

Polling

// Client
function checkUpdates(url) {
    fetch(url)
        .then(response => {
            if (!response.ok) {
                throw new Error('Network response was not ok');
            }
            return response.json();
        })
        .then(data => {
            if (data.newBid) {
                alert('New bid placed: ' + data.bidAmount);
            }
        })
        .catch(error => {
            console.error('There was a problem with the fetch operation:', error);
        });
}

setInterval(() => checkUpdates('/auction/updates'), 60000); // every 60 seconds

// Server (Express)
import express from "express";
const app = express();

let message = null;

app.get("/poll", (req, res) => {
    res.json({ message });
});

app.get("/send", (req, res) => {
    message = "Hello from server";
    res.send("sent");
});

app.listen(3000);

Trade-off: dead simple to implement, but wasteful. Most requests come back with "nothing changed," and there's always a delay between something happening and the client finding out — bounded by however long you set the interval.

2. Long Polling

Long polling flips the waiting around. The client still initiates the request, but instead of the server replying immediately with "nothing yet," it holds the connection open until there's actually something to send — then responds, and the client immediately opens a new request to keep listening.

Long Polling

// Server (Express)
import express from "express";
const app = express();

let waitingClients = [];

app.get("/long-poll", (req, res) => {
    waitingClients.push(res);
});

app.get("/send", (req, res) => {
    waitingClients.forEach(client =>
        client.json({ message: "New data arrived" })
    );
    waitingClients = [];
    res.send("sent");
});

app.listen(3000);

// Client
async function longPoll() {
    const res = await fetch("http://localhost:3000/long-poll");
    const data = await res.json();
    console.log("Long polling:", data.message);
    longPoll(); // immediately re-request
}

longPoll();

Trade-off: much less wasted traffic than plain polling, and updates arrive close to instantly. The cost is that the server has to keep a request (and its resources) open per waiting client, which can get expensive at scale.

3. Server-Sent Events (SSE)

SSE flips the model entirely: instead of the client asking, the server pushes data down over a single long-lived HTTP connection. It's one-way only — server to client — which makes it a great fit for live feeds, notifications, or dashboards where the browser never needs to talk back.

Server-Sent Events

// Server (Express)
import express from "express";
const app = express();

app.get("/events", (req, res) => {
    res.setHeader("Content-Type", "text/event-stream");

    setInterval(() => {
        res.write(`data: ${JSON.stringify({ time: Date.now() })}\n\n`);
    }, 3000);
});

app.listen(3000);

// Client (browser only — no server-side EventSource by default)
const eventSource = new EventSource("http://localhost:3000/events");

eventSource.onmessage = (event) => {
    console.log("SSE:", event.data);
};

Trade-off: simpler than WebSockets to set up (it's just HTTP under the hood, so it plays nicely with proxies and firewalls), with automatic reconnection built into the browser's EventSource API. The catch is it's strictly one-directional — if the client needs to send data back, you still need a separate regular request or a different protocol entirely.

4. WebSockets

WebSockets open a single persistent connection where both sides can send messages whenever they want — true full-duplex, real-time communication. This is the pattern behind chat apps, multiplayer games, and live trading dashboards.

WebSockets

// Server
import { WebSocketServer } from "ws";

const wss = new WebSocketServer({ port: 3000 });

wss.on("connection", (ws) => {
    ws.on("message", (msg) => {
        console.log("Client:", msg.toString());
    });

    ws.send("Hello client");
});

// Client
const ws = new WebSocket("ws://localhost:3000");

ws.onmessage = (event) => {
    console.log("Server:", event.data);
};

ws.onopen = () => {
    ws.send("Hello server");
};

Trade-off: the most powerful and flexible option here, but also the most involved — you're managing a stateful, persistent connection, handling reconnects yourself, and thinking about scaling connections across multiple server instances (often via something like Redis pub/sub).

Quick Comparison

Pattern Direction What it actually does
Polling Client → Server (repeated) Asks over and over on a timer
Long Polling Client → Server (held open) Asks once, server waits to reply until there's news
SSE Server → Client only Server streams updates down automatically
WebSocket Client ↔ Server Both sides talk over one open connection, anytime

Which One Should You Use?

Start with the simplest thing that solves the problem:

  • Need something dead simple, and updates every few seconds/minutes is fine? Polling.
  • Need near-instant updates but they're infrequent, and you don't want a whole WebSocket setup? Long Polling.
  • Only the server needs to push data, one direction, and you want something that just works over plain HTTP? SSE.
  • Both sides need to talk in real time — chat, games, collaborative editing? WebSockets.

Conclusion

None of these patterns is strictly "better" — they're different answers to the same question: how do you get data from the server to the client (and back) without the client sitting there blocked and waiting? Understanding the trade-off each one makes between simplicity, server load, and how "real-time" the result actually is makes it much easier to pick the right tool instead of reaching for WebSockets by default.

Conclusion
← All postsBrowse tags →