Skip to main content

Real-Time APIs: WebSockets vs SSE vs Long Polling 2026

·APIScout Team
Share:

Building Real-Time APIs: WebSockets vs SSE vs Long Polling

Real-time data — live dashboards, chat messages, stock prices, notifications, collaborative editing — requires pushing data from server to client without the client repeatedly asking "anything new?" Three patterns dominate: WebSockets, Server-Sent Events (SSE), and long polling.

The Three Patterns

WebSockets

What: Full-duplex TCP connection. Both client and server can send messages at any time.

Connection: HTTP upgrade handshake → persistent TCP connection

Client: GET /ws (Upgrade: websocket)
Server: 101 Switching Protocols
─── Full-duplex communication ───
Client → Server: {"type": "subscribe", "channel": "prices"}
Server → Client: {"symbol": "AAPL", "price": 185.50}
Client → Server: {"type": "unsubscribe", "channel": "prices"}

Strengths:

  • Bidirectional — client and server both send freely
  • Lowest latency — no HTTP overhead per message
  • Binary and text support
  • Best for high-frequency updates (trading, gaming, chat)

Weaknesses:

  • Stateful — each connection must be maintained server-side
  • Load balancing is complex (sticky sessions or connection state sharing)
  • Reconnection logic is on the client
  • No built-in multiplexing (one connection per WebSocket endpoint)
  • Proxy/firewall issues in some enterprise environments
  • Memory overhead per connection (~2-10KB per connection)

Server-Sent Events (SSE)

What: Unidirectional stream from server to client over HTTP. Client subscribes, server pushes events.

Connection: Standard HTTP request → server keeps connection open, sends events

Client: GET /events (Accept: text/event-stream)
Server: 200 OK (Content-Type: text/event-stream)
─── Server pushes events ───
Server → Client: event: price\ndata: {"symbol": "AAPL", "price": 185.50}\n\n
Server → Client: event: price\ndata: {"symbol": "AAPL", "price": 185.55}\n\n

Strengths:

  • Simple — standard HTTP (works through proxies, CDNs, load balancers)
  • Auto-reconnection — browser EventSource API reconnects automatically
  • Event IDs — server can resume from where client disconnected
  • Lightweight — less overhead than WebSocket for server-to-client only
  • CDN-cacheable (with proper headers)

Weaknesses:

  • Unidirectional only — server to client (client sends via regular HTTP requests)
  • Text only (no binary)
  • Connection limit — browsers limit to 6 SSE connections per domain (HTTP/1.1)
  • No built-in authentication (cookies or URL tokens)
  • Less tooling than WebSockets

Long Polling

What: Client makes a request. Server holds it open until data is available or timeout. Client immediately reconnects.

Connection: Repeated HTTP requests

Client: GET /updates?since=123
Server: (waits up to 30 seconds)
Server: 200 OK {"data": [...], "last_id": 456}
Client: GET /updates?since=456
Server: (waits...)

Strengths:

  • Works everywhere — standard HTTP, no special protocols
  • No firewall/proxy issues
  • Simple server implementation (just delay the response)
  • Stateless — each request is independent

Weaknesses:

  • Higher latency — round-trip per update
  • Higher bandwidth — HTTP headers on every request
  • Server resource consumption — holding connections open
  • Not truly real-time — gaps between polls
  • Scaling is inefficient compared to WebSocket/SSE

Comparison Table

FeatureWebSocketSSELong Polling
DirectionBidirectionalServer → ClientServer → Client
ProtocolWebSocket (TCP)HTTPHTTP
LatencyLowestLowMedium
ReconnectionManualAutomaticManual
Binary data✅ Yes❌ No❌ No
HTTP/2 multiplexing❌ No✅ Yes✅ Yes
CDN-friendly❌ No✅ Yes✅ Yes
Proxy-friendly⚠️ Sometimes✅ Yes✅ Yes
Browser connectionsUnlimited6/domain (HTTP/1.1)6/domain
Server memory~2-10KB/conn~1-5KB/connMinimal
Scalability⚠️ Stateful⚠️ Stateful✅ Stateless

Decision Framework

Choose WebSocket When:

  • Bidirectional communication — chat, multiplayer games, collaborative editing
  • High-frequency updates — trading platforms, live sports, real-time analytics
  • Binary data — file transfer, audio/video streaming
  • Client-initiated messages — the client needs to send data frequently

Choose SSE When:

  • Server-to-client only — notifications, live feeds, dashboards
  • Standard HTTP infrastructure — CDNs, load balancers, proxies
  • Auto-reconnection needed — EventSource handles reconnection automatically
  • AI/LLM streaming — OpenAI, Anthropic, and most AI APIs use SSE for streaming responses

Choose Long Polling When:

  • Maximum compatibility — must work everywhere, including restrictive networks
  • Infrequent updates — updates every 30-60 seconds, not milliseconds
  • Simple implementation — no WebSocket infrastructure, no SSE support issues
  • Stateless architecture — don't want to maintain connection state

Real-World Usage

CompanyTechnologyUse Case
SlackWebSocketReal-time messaging
DiscordWebSocketChat, voice, presence
OpenAISSEStreaming LLM responses
AnthropicSSEStreaming Claude responses
GitHubSSELive issue/PR updates
StripeWebSocketDashboard real-time data
Twitter/XSSEStreaming API
FirebaseWebSocketRealtime Database
VercelSSEBuild log streaming

Scaling Considerations

WebSocket Scaling

  • Requires sticky sessions (or shared connection state)
  • Connection count is limited by server memory and file descriptors
  • Use Redis pub/sub or NATS for cross-server message routing
  • Consider managed services: Pusher, Ably, Socket.IO

SSE Scaling

  • Standard HTTP — works with existing load balancers
  • HTTP/2 multiplexes connections (solves 6-connection limit)
  • Can be CDN-cached with proper headers
  • Easier to scale than WebSockets

When to Use a Managed Service

If you need 10K+ concurrent connections, consider a managed real-time service instead of building your own:

ServiceStrengths
PusherSimple pub/sub, great SDK
AblyEnterprise, guaranteed ordering
Socket.IOSelf-hosted, most popular
Supabase RealtimePostgres changes → clients
Firebase Realtime DBReal-time sync, offline

WebSocket Implementation in Node.js

The ws library is the most widely used WebSocket server for Node.js. A minimal production-ready server handles connection lifecycle, message parsing, and heartbeats to detect dropped connections:

import { WebSocketServer, WebSocket } from 'ws';
import http from 'http';

const server = http.createServer();
const wss = new WebSocketServer({ server });

// Track connected clients with metadata
const clients = new Map<WebSocket, { userId: string; room: string }>();

wss.on('connection', (ws, req) => {
  // Authenticate on connection (before accepting messages)
  const token = new URL(req.url!, 'ws://localhost').searchParams.get('token');
  const userId = verifyToken(token); // throw → WebSocket closes
  if (!userId) { ws.close(4001, 'Unauthorized'); return; }

  clients.set(ws, { userId, room: 'default' });

  ws.on('message', (data) => {
    const msg = JSON.parse(data.toString());
    if (msg.type === 'join') {
      clients.get(ws)!.room = msg.room;
    }
    if (msg.type === 'broadcast') {
      // Fan out to everyone in the same room
      for (const [client, meta] of clients) {
        if (meta.room === clients.get(ws)!.room && client.readyState === WebSocket.OPEN) {
          client.send(JSON.stringify({ type: 'message', from: userId, text: msg.text }));
        }
      }
    }
  });

  ws.on('close', () => clients.delete(ws));
  // Heartbeat: detect stale connections
  ws.isAlive = true;
  ws.on('pong', () => { ws.isAlive = true; });
});

// Ping all clients every 30s; close unresponsive ones
setInterval(() => {
  for (const [ws] of clients) {
    if (!ws.isAlive) { ws.terminate(); clients.delete(ws); continue; }
    ws.isAlive = false;
    ws.ping();
  }
}, 30_000);

The heartbeat interval catches clients that disconnected without sending a close frame — mobile clients on flaky networks are the common case. Without it, dead connections accumulate and consume server memory until the process restarts.

SSE in Next.js App Router

SSE is the right choice for AI streaming responses, notification feeds, and any server-to-client push where the client doesn't send messages back. The Next.js App Router makes SSE straightforward with ReadableStream:

// app/api/events/route.ts
export async function GET(req: Request) {
  const encoder = new TextEncoder();

  const stream = new ReadableStream({
    async start(controller) {
      const send = (event: string, data: unknown) => {
        controller.enqueue(encoder.encode(`event: ${event}\ndata: ${JSON.stringify(data)}\n\n`));
      };

      // Send initial connection confirmation
      send('connected', { timestamp: Date.now() });

      // Subscribe to your event source (Redis, database, etc.)
      const unsubscribe = subscribeToEvents((event) => {
        send(event.type, event.payload);
      });

      // Clean up when client disconnects
      req.signal.addEventListener('abort', () => {
        unsubscribe();
        controller.close();
      });
    },
  });

  return new Response(stream, {
    headers: {
      'Content-Type': 'text/event-stream',
      'Cache-Control': 'no-cache, no-transform',
      'Connection': 'keep-alive',
    },
  });
}
// Client side
const eventSource = new EventSource('/api/events');
eventSource.addEventListener('notification', (e) => {
  const data = JSON.parse(e.data);
  addNotification(data);
});
eventSource.onerror = () => {
  // EventSource automatically reconnects after 3 seconds by default
  // Use Last-Event-ID header to resume from the last seen event
};

Authentication for Real-Time Connections

Both WebSockets and SSE require explicit authentication handling. HTTP cookies work naturally (browser sends them automatically on connection), but token-based auth requires care:

For WebSockets, pass the token as a query parameter on the upgrade request URL (ws://example.com/chat?token=...). The token appears in server access logs — use short-lived tokens (5 minutes, single-use) generated specifically for the WebSocket handshake. A common pattern is a dedicated endpoint that exchanges a session token for a short-lived WebSocket ticket, which the client uses within the next 60 seconds.

For SSE, the EventSource API does not support custom headers, which is the main authentication limitation. Solutions: pass a token as a query parameter, rely on httpOnly session cookies (the simplest approach for same-domain applications), or use a POST-based SSE alternative where the client posts credentials to get a stream.

In both cases, revalidate authentication on reconnection. Tokens that were valid when the connection was opened may expire during a long-lived session. Implementing a token refresh mechanism that the server signals (via a reauthenticate event for SSE, or a special message type for WebSockets) prevents silent authentication failures for long-running connections.

Error Handling and Reconnection Strategies

Network interruptions are normal. A production real-time implementation should handle them transparently:

For SSE, the browser's EventSource handles reconnection automatically using the retry field (which the server can send to control the reconnection delay) and the id field (which the server sends with each event; the browser includes the last-seen ID as Last-Event-ID on reconnection, letting the server resume from where it left off). This built-in resumability is a genuine advantage over WebSockets, which require client-side reconnection logic.

For WebSockets, implement exponential backoff with jitter — attempt 1 after 1s, attempt 2 after 2s, attempt 3 after 4s, up to a maximum of 30s between attempts. Jitter (randomizing the delay by ±25%) prevents thundering-herd reconnections when many clients disconnect simultaneously (server restart, load balancer restart). After reconnection, send a catch-up request with the last event ID received so the server can replay any missed updates.

Choosing a Pattern in Practice

The theoretical differences between WebSockets, SSE, and long polling are clear in documentation, but the practical decision is often driven by infrastructure constraints as much as technical requirements.

HTTP/2 changes the SSE equation significantly. The 6-concurrent-connection browser limit that made SSE awkward for applications with many event streams applies only to HTTP/1.1. Under HTTP/2, browser connections are multiplexed — you can have dozens of SSE streams over a single TCP connection. If your infrastructure uses HTTP/2 (which it should, for performance), SSE's connection limit is no longer a meaningful constraint.

CDN compatibility is a real advantage for SSE. WebSocket connections bypass most CDN caching layers; CDNs generally proxy WebSocket traffic without caching it. SSE responses over HTTP can be handled by CDNs and edge networks, which enables geographic distribution of event sources without sticky session routing. For applications that push data to globally distributed users — news feeds, sports scores, public dashboards — this is a meaningful scalability advantage.

Load balancers need explicit WebSocket support. Not all load balancers handle WebSocket upgrades correctly, particularly older enterprise load balancers and managed services. AWS Application Load Balancer, nginx, and Cloudflare all support WebSockets, but the configuration is explicit rather than automatic. If you're deploying to a managed platform (Fly.io, Railway, Render), verify WebSocket support before committing to the pattern.

For AI streaming specifically, use SSE. OpenAI's chat completions streaming, Anthropic's Claude streaming, and virtually every other LLM streaming API uses SSE. The pattern is established; client libraries handle it correctly; and the server-to-client unidirectional flow matches the use case perfectly.

Long polling still wins in one real scenario. Legacy enterprise environments with aggressive proxy servers and strict firewall rules sometimes block WebSocket upgrades and SSE streams as non-standard traffic. In B2B enterprise applications deployed behind corporate proxies, long polling is the only option that works everywhere without IT cooperation. Modern consumer applications don't face this constraint.

The most common production architecture for 2026: WebSockets for user-to-user interactive features (chat, collaborative editing, multiplayer), SSE for server-to-client push (notifications, AI streaming, live feeds), and long polling only as a compatibility fallback for restricted network environments. Each pattern solves a different problem — using all three in the same application is normal and correct.

Methodology

Comparison table data and performance characteristics are based on MDN Web Docs (WebSocket and EventSource specifications), the WebSocket RFC (RFC 6455), and W3C Server-Sent Events specification. Browser connection limits reflect Chromium and Firefox behavior as of early 2026 (both resolve the 6-connection per-domain limit under HTTP/2). Managed service feature comparisons sourced from Pusher, Ably, and Socket.IO documentation as of March 2026. Code examples tested against Node.js 22 LTS and Next.js 15.


Building real-time APIs? Explore Pusher, Ably, and real-time API tools on APIScout — comparisons, guides, and developer resources.

Related: OpenAI Realtime API: Building Voice Applications 2026, How to Design REST APIs Developers Love 2026, Build a Real-Time Dashboard with the Mixpanel API

The API Integration Checklist (Free PDF)

Step-by-step checklist: auth setup, rate limit handling, error codes, SDK evaluation, and pricing comparison for 50+ APIs. Used by 200+ developers.

Join 200+ developers. Unsubscribe in one click.