Written by

Halkwinds Editorial Team

Halkwinds Research & Editorial

Published June 11, 2026
Blog image
Application

Building Real-Time Applications: WebSockets, Server-Sent Events, and Long Polling

When and how to implement bidirectional and push-based communication — with performance trade-offs and scaling considerations.

Every engineering manager eventually inherits a feature request that starts with "we need it to update in real time." A live dashboard, a collaborative editor, a trading ticker, a chat feature, or push notifications that don't require a page refresh. The tricky part isn't deciding that you need real-time behavior — it's choosing the right transport, planning for scale before it bites you, and keeping the operational complexity from swallowing your team. This guide walks through the three dominant approaches to building real-time web applications — WebSockets, Server-Sent Events (SSE), and long polling — with concrete trade-offs, an implementation path, and the scaling gotchas that surface in production.

  • Background / Why This Matters
  • Prerequisites and Planning
  • Step-by-Step Implementation
  • Testing and Validation
  • Common Mistakes / What to Avoid
  • Frequently Asked Questions
  • Conclusion

Background / Why This Matters

The classic request/response model of HTTP assumes the client asks and the server answers. That works for most CRUD apps, but it breaks down when the server needs to initiate communication — a new message arrives, a stock price moves, a build finishes. For years teams worked around this with polling: the browser asks "anything new?" every few seconds. It works, but it wastes bandwidth, adds latency, and hammers your infrastructure with mostly-empty responses.

Three techniques emerged to solve this properly, and each fits a different shape of problem:

  • Long polling — the client makes a request that the server holds open until data is available, then immediately reconnects. A pragmatic fallback that works everywhere.
  • Server-Sent Events (SSE) — a one-way, server-to-client stream over a single long-lived HTTP connection, using the native EventSource API.
  • WebSockets — a full-duplex, bidirectional channel that stays open for the life of the session.

Choosing correctly matters because the wrong transport creates cost and complexity you'll carry for years. WebSockets are powerful but introduce stateful connections that complicate load balancing and horizontal scaling. SSE is elegant for one-directional streams but has connection limits over HTTP/1.1. Long polling is universally compatible but inefficient at scale.

Feature Long Polling Server-Sent Events WebSockets
Direction Client-initiated Server → Client only Bidirectional
Protocol HTTP HTTP ws:// / wss:// (upgraded from HTTP)
Auto-reconnect Manual Built-in (EventSource) Manual / library-provided
Binary support Limited Text only (base64 for binary) Native binary + text
Proxy/firewall friendliness Excellent Good Occasional issues (needs wss)
Overhead per message High (full HTTP cycle) Low Very low (2–14 byte frame)
Best fit Legacy fallback, low-frequency updates Feeds, notifications, dashboards Chat, gaming, collaboration

Takeaway: If your data flows in one direction (server pushing updates), start with SSE. If you need true bidirectional exchange, use WebSockets. Keep long polling in your back pocket as a fallback for constrained environments.

Prerequisites and Planning

Before writing a single line of connection code, get your engineering team aligned on the following. Skipping this planning phase is the single most common reason real-time projects blow their timelines.

1. Define the actual latency requirement

"Real time" is a spectrum. A collaborative cursor needs sub-100ms updates; a notifications badge is fine at 1–2 seconds. Estimates vary, but the tighter your latency budget, the more you'll pay in infrastructure and complexity. Write down a concrete number — it drives every subsequent decision.

2. Estimate concurrent connections

WebSocket and SSE connections are persistent. A server that comfortably handles 10,000 request/response cycles per second may struggle with 50,000 simultaneously open sockets because each connection consumes file descriptors, memory, and event-loop attention. Model your peak concurrency, not your request volume.

3. Plan for horizontal scale from day one

The moment you run more than one server instance, you have a fan-out problem: a message arriving on Server A must reach clients connected to Server B. This is where Redis Pub/Sub (or a managed alternative like a message broker) becomes essential. Design the fan-out layer before you build the connection layer.

4. Decide your authentication model

Persistent connections can't rely on refreshing an HTTP session cookie per request. Plan how you'll authenticate the initial handshake (typically a JWT passed as a query parameter for SSE or in the connection message for WebSockets) and how you'll revoke access mid-session.

At Halkwinds, our Application engineering team routinely starts real-time projects with a one-page architecture doc covering these four points. It takes an afternoon and prevents weeks of rework.

Takeaway: Nail down latency targets, peak concurrency, the fan-out strategy, and auth before implementation. These decisions are expensive to reverse.

Step-by-Step Implementation

Below is a pragmatic implementation path using Node.js, which handles thousands of concurrent connections well thanks to its event-driven model. The same principles apply to Go, Elixir, or Java.

Step 1: Start with the simplest transport that meets your need

If you only push data server-to-client — dashboards, activity feeds, price tickers — implement SSE first. It's dramatically simpler than WebSockets: a standard HTTP endpoint that sets Content-Type: text/event-stream, keeps the connection open, and writes data: lines. The browser's native EventSource handles reconnection and event parsing for free.

  1. Create an HTTP route that sets the SSE headers and never closes.
  2. On the client, instantiate new EventSource('/events') and attach an onmessage handler.
  3. Send periodic comment lines (: keepalive) every ~15 seconds to prevent proxy timeouts.

Step 2: Use WebSockets when you need bidirectional traffic

For chat, collaborative editing, or interactive gaming, reach for WebSockets. In Node.js the ws library is the low-level workhorse, while Socket.IO adds automatic reconnection, room abstractions, and long-polling fallback out of the box. For most product teams, the higher-level library saves meaningful time — but be aware it uses its own wire protocol, so both ends must speak it.

  • Perform the HTTP upgrade handshake and authenticate the token during connection.
  • Establish a heartbeat (ping/pong) so you detect dead connections rather than leaking them.
  • Group connections into logical channels ("rooms") so you can target broadcasts efficiently.

Step 3: Add a Redis fan-out layer for multiple instances

Once you scale past a single process, wire up Redis Pub/Sub. Each server instance subscribes to relevant channels. When an event occurs on any instance, it publishes to Redis; every instance receives it and forwards it to its locally connected clients. Socket.IO ships an official Redis adapter that does exactly this — enabling it is often a two-line change plus a Redis endpoint.

Step 4: Configure your load balancer and infrastructure

Persistent connections need special handling:

  • Enable sticky sessions (or use a stateless design with Redis) so a client stays bonded to one instance where required.
  • Terminate TLS and always use wss:// in production — plain ws:// is frequently blocked by corporate proxies.
  • Raise idle-connection timeouts on your load balancer (AWS ALB defaults to 60 seconds; long-lived streams need higher, plus keepalive pings).
  • Increase OS-level file descriptor limits, since each connection consumes one.

Step 5: Handle push notifications separately

A common conflation: push notifications to a device (delivered even when your app isn't open) are not WebSockets. They use the Web Push API with a service worker on the browser, or APNs/FCM on mobile. Use WebSockets/SSE for in-app live updates; use the push services for out-of-app delivery. Trying to keep a socket alive to power notifications drains batteries and doesn't survive app closure.

Takeaway: Layer your architecture — transport (SSE/WebSockets), fan-out (Redis), infrastructure (sticky sessions, wss, timeouts), and separately, device push. Don't force one tool to do all four jobs.

Testing and Validation

Real-time systems fail in ways that unit tests rarely catch. Build validation into your process:

  • Load test concurrency, not throughput. Tools like Artillery (which supports WebSocket and SSE scenarios) or k6 can open tens of thousands of simultaneous connections. Watch memory and file-descriptor growth, not just requests per second.
  • Simulate network chaos. Drop connections, throttle bandwidth, and introduce latency to confirm reconnection and message-replay logic works. Verify no messages are silently lost during a reconnect.
  • Test the fan-out. Run at least two server instances locally and confirm a message sent through one reaches clients on the other via Redis.
  • Measure end-to-end latency. Timestamp messages at publish and at client receipt to confirm you're actually hitting your latency budget under load