SoftwareCrafting Logo

WebSockets vs SSE vs Polling: Which Real-Time Technology Should You Use?

DDeepak RajputBackend15 min read21 Aug 2026
Diagram comparing bidirectional WebSocket traffic, one-way SSE streaming, and repeated polling between a server and browser

TL;DR: Use WebSockets when both sides need low-latency bidirectional communication. Use SSE when the server streams text updates to a browser and the client mostly sends ordinary HTTP requests. Use polling when simplicity, compatibility, or infrequent updates matter more than connection efficiency.

Why the choice matters

“Real-time” describes a user expectation, not a protocol. A live trading screen, a chat room, a build-progress indicator, and an unread notification badge all have different communication needs. Choosing WebSockets for every feature can add unnecessary connection management, infrastructure cost, and operational complexity.

The right decision depends on four questions:

  • Does the client need to send messages over the same long-lived connection?
  • How quickly must updates arrive?
  • Can the network, proxy, and hosting environment keep connections open?
  • How much complexity can the product and operations team support?

The four patterns

Short polling sends a request at a fixed interval, such as every ten seconds. It is easy to understand and works through normal HTTP infrastructure, but it creates requests even when nothing changes.

Long polling holds an HTTP request open until data is available or a timeout occurs. It reduces empty responses but requires the client to reconnect after every response and needs careful timeout handling.

Server-Sent Events use a long-lived HTTP response for server-to-client text events. The browser receives an EventSource stream and automatically attempts reconnection. The client still uses normal HTTP for commands.

WebSockets upgrade an HTTP connection into a persistent, bidirectional channel. Both client and server can send messages whenever they need to, which makes the protocol suitable for interactive state changes.

Decision table

PatternDirectionBest forMain cost
Short pollingRequest and responseInfrequent status checks and simple admin toolsRepeated requests and stale intervals
Long pollingMostly server to clientLegacy compatibility and moderate updatesTimeout and reconnect management
SSEServer to clientDashboards, progress, notifications, streamed textOne-way text stream and proxy timeout concerns
WebSocketsBidirectionalChat, collaboration, multiplayer, live controlsConnection state, scaling, and observability

Implement SSE for a progress stream

SSE is often the simplest choice for a browser waiting for a long-running job. The command can be a normal POST, while the progress updates arrive on a separate stream.

import type { Request, Response } from 'express';

export async function streamJob(req: Request, res: Response) {
  const job = await jobs.findVisible(req.user.id, req.params.jobId);
  if (!job) return res.sendStatus(404);

  res.writeHead(200, {
    'Content-Type': 'text/event-stream',
    'Cache-Control': 'no-cache, no-transform',
    Connection: 'keep-alive',
  });

  const send = (event: string, data: unknown) => {
    res.write(`event: ${event}\ndata: ${JSON.stringify(data)}\n\n`);
  };

  const unsubscribe = jobEvents.subscribe(job.id, (update) => send('progress', update));
  send('ready', { jobId: job.id });

  req.on('close', () => {
    unsubscribe();
    res.end();
  });
}

Send periodic heartbeats if a proxy may close idle connections. Never assume the connection stays alive. The browser must reconnect and resume from a known event ID or fetch the current job state after reconnecting.

The existing real-time dashboard with React and Node.js is a useful implementation companion, while this article focuses on choosing the transport.

Implement WebSockets for interaction

WebSockets fit a shared whiteboard where many clients send cursor positions and receive changes. A server still needs authentication during the handshake, message validation, authorization, and a clear protocol for joining and leaving rooms.

import { WebSocketServer } from 'ws';
import { z } from 'zod';

const messageSchema = z.discriminatedUnion('type', [
  z.object({ type: z.literal('join'), roomId: z.string().uuid() }),
  z.object({ type: z.literal('cursor'), x: z.number(), y: z.number() }),
]);

const wss = new WebSocketServer({ noServer: true });

wss.on('connection', (socket, user) => {
  socket.on('message', (raw) => {
    const parsed = messageSchema.safeParse(JSON.parse(raw.toString()));
    if (!parsed.success) {
      socket.send(JSON.stringify({ type: 'error', code: 'INVALID_MESSAGE' }));
      return;
    }

    // Check room membership before broadcasting parsed.data.
  });
});

Do not broadcast raw messages to every client. Map each connection to authenticated rooms and publish only events the user can see. For high-frequency data such as cursor movement, throttle updates and discard stale positions.

Polling is sometimes the correct choice

Polling is a good fit when updates are rare, freshness of several seconds is acceptable, or the client environment is unreliable. It is also a useful fallback for mobile networks and constrained enterprise proxies. An endpoint that returns an updatedAt or version lets the server respond with 304 Not Modified or a small unchanged response.

Avoid synchronized polling storms. Add jitter to intervals, use exponential backoff after errors, and pause polling when the browser tab is hidden. If a job can take minutes, prefer a status endpoint with backoff over a one-second interval.

Scaling connection-heavy systems

HTTP polling scales through ordinary request infrastructure, but request volume can be high. SSE and WebSockets consume long-lived connections, file descriptors, memory, and load-balancer capacity. Set explicit connection limits and idle timeouts.

When multiple application instances serve a shared room, a client connected to instance A must receive events published by instance B. Use a broker, managed real-time service, or a durable event stream. Redis Pub/Sub can distribute ephemeral events, while a durable system is better when reconnecting clients must catch up. Do not assume an in-memory event emitter works after horizontal scaling.

Use sticky sessions only when the protocol or state design truly requires them. A stateless connection gateway with external room state is easier to operate, but it must handle membership races and reconnects.

Reliability and client behavior

Every real-time client needs a state machine: disconnected, connecting, connected, degraded, and closed. Reconnect with exponential backoff and jitter. After reconnecting, request a snapshot or replay missed events from a cursor. The server should treat duplicate client messages as possible and make commands idempotent where practical.

For user-visible data, real-time delivery should be an optimization, not the only source of truth. Store the authoritative state in a database or service. If an update is missed, the client can refetch and converge.

Security and observability

Authenticate the handshake or stream request, validate every message, enforce authorization at the room or resource level, and limit message size and frequency. A WebSocket connection is not a permission grant. Recheck access when a user is removed from a workspace.

Track active connections, reconnects, messages per connection, stream duration, send queue depth, dropped messages, p95 delivery latency, and authorization failures. Correlate connection IDs with user and tenant IDs without logging message contents by default.

Common mistakes

  • Using WebSockets when the browser only needs server-to-client updates.
  • Treating SSE as reliable storage instead of a delivery channel.
  • Forgetting reconnect and resynchronization behavior.
  • Keeping room state only in one application process.
  • Sending unvalidated JSON directly to other clients.
  • Allowing an idle connection to consume resources indefinitely.

Production checklist

  • The protocol matches the direction and latency requirement.
  • Authentication and authorization are enforced per connection and message.
  • Reconnect, backoff, heartbeat, and resync behavior are tested.
  • Connection and message limits are configured.
  • Multi-instance fanout has an explicit design.
  • The database or service remains the source of truth.
  • Delivery latency, reconnects, drops, and errors are observable.

Conclusion

Choose the simplest transport that meets the product requirement. Polling is often enough, SSE is excellent for browser streams, and WebSockets are valuable when both sides need to communicate continuously. SoftwareCrafting’s real-time messaging service can help design the protocol, event model, and scaling strategy for production applications.

Model the event contract

Transport selection does not define the application protocol. Define event names, payload schemas, versioning, ordering guarantees, replay behavior, and authorization rules separately. A client should know whether an event is a complete snapshot, a patch, or a notification that tells it to refetch. Include a monotonic version or cursor when clients need to detect gaps.

Backpressure and unreliable clients

A client on a poor network can read more slowly than the server produces events. Set a maximum per-connection queue, drop or coalesce events that are safe to replace, and close a connection that cannot keep up after sending a recoverable error. For audit events, never drop data. Store them durably and provide cursor-based replay.

Browsers suspend background tabs, mobile operating systems pause processes, and corporate proxies terminate idle streams. Clients must reconnect, refresh authentication, and fetch a snapshot after a long absence. Test tab suspension, network switching, VPNs, expired credentials, and reconnect storms.

Capacity testing

Load-test idle connections, message bursts, slow consumers, reconnect storms, and a broker outage. Measure file descriptors, memory per connection, event-loop delay, broker fanout, database pressure, and recovery time. A system that handles many idle connections may still fail when all clients reconnect after a regional network interruption.

Expose a normal status endpoint even when the primary experience uses SSE or WebSockets. It gives clients a recovery path and makes support diagnostics easier. For expensive jobs, use backoff intervals with jitter and stop after a defined maximum.

About the author

Deepak Rajput

This article was published by SoftwareCrafting engineers for founders, product teams, and developers working on real production delivery. We focus on practical tradeoffs, maintainable architecture, and implementation details that hold up outside demos.

View author profile

Last updated: 2026-08-21