BytePane

WebSocket vs REST: When to Use Each API Protocol

API17 min read

Key Takeaways

  • WebSocket handles ~4,000 msg/sec vs ~10 req/sec for HTTP REST at high frequency — per FeathersJS benchmarks
  • REST cannot push: every update requires the client to initiate a request — polling is the only option
  • WebSocket scales poorly horizontally — stateful connections require sticky sessions + pub/sub brokers across instances
  • For server-to-client only streams, prefer Server-Sent Events (SSE) — simpler, works through HTTP/2, auto-reconnects
  • Best architecture: REST for auth, CRUD, and data fetching; WebSocket/SSE for real-time event delivery

The Wrong Question: "Which Is Better?"

Every few months, a developer rebuilds their entire REST API as WebSocket and then writes a blog post about how much worse it got. WebSocket is not a better REST. It is a fundamentally different communication primitive that solves a problem REST cannot: server-initiated message delivery.

REST is a request-response architecture built on HTTP. The client asks, the server answers. The server cannot speak unless spoken to. WebSocket is a persistent bidirectional channel: once the connection is established, either side can transmit at any time with minimal overhead. A message costs just 2–14 bytes of framing header, versus the hundreds of bytes of HTTP headers on every REST request.

The right question is: does your application need the server to push updates to clients without a client request? If yes, WebSocket (or SSE). If no, REST is almost certainly the better choice — simpler to build, scale, debug, and cache.

How WebSocket Works: The Upgrade Handshake

WebSocket starts as an HTTP/1.1 request and upgrades to a persistent TCP connection via the Upgrade header. This upgrade mechanism is defined in RFC 6455 (the WebSocket Protocol specification, published 2011 by the IETF). After the handshake, the HTTP protocol is abandoned and the raw TCP socket is used for framed messages.

// WebSocket upgrade handshake (HTTP/1.1)
// Client → Server:
GET /ws HTTP/1.1
Host: api.example.com
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
Sec-WebSocket-Version: 13

// Server → Client (101 Switching Protocols):
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=

// After this: raw TCP socket, no more HTTP
// Each message has a 2-14 byte frame header:
//  - FIN bit (final fragment)
//  - Opcode (text/binary/ping/pong/close)
//  - Mask bit + masking key (client→server must be masked per RFC 6455)
//  - Payload length (7, 16, or 64 bits)

// Compare to HTTP REST message overhead:
// HTTP/1.1 headers: ~400-800 bytes per request (cookies, user-agent, etc.)
// WebSocket frame header: 2-14 bytes per message
// For 100-byte messages: WS overhead = 2-12%, HTTP overhead = 400-800%

The framing overhead difference is the source of WebSocket's throughput advantage for high-frequency small messages. For a 100-byte message payload, WebSocket adds 2–14 bytes (2–12% overhead) versus HTTP's 400–800 bytes of headers (400–800% overhead). Once your message rate drops below roughly one per second, this advantage disappears — HTTP/2 multiplexing over a persistent connection closes the gap significantly.

Performance Data: Real Numbers

The most-cited benchmark data comes from David Luecke's HTTP vs WebSockets comparison (published on the FeathersJS engineering blog), which compared Socket.io against HTTP REST on identical Node.js servers with identical payloads.

MetricHTTP RESTSocket.io (WebSocket)WebSocket Advantage
50 requests completion~5,000ms~180ms27x faster
Throughput (req/s)~10 req/s~4,000 msg/s400x higher
Connection overheadPer request (TCP + TLS)Once per sessionAmortized to zero
Message header overhead400–800 bytes2–14 bytes200–400x smaller
CDN cachingFull support (GET requests)Not applicableREST wins
Horizontal scalingTrivial (stateless)Complex (sticky sessions + pubsub)REST wins
Server pushImpossible (polling required)NativeWebSocket wins

These numbers represent the extreme high-frequency case. In real applications, the performance gap narrows considerably. HTTP/2 multiplexes multiple requests over a single TCP connection, reducing per-request TCP/TLS overhead significantly. For applications that make one API call per user action, REST over HTTP/2 and WebSocket are functionally equivalent in latency.

The Problem REST Cannot Solve: Server Push

Consider a collaborative text editor like Google Docs. User A types a character. User B's screen must update within 100ms to feel real-time. With REST:

// REST polling attempt at "real-time":
// Client B must ask the server for changes, repeatedly
setInterval(async () => {
  const changes = await fetch('/api/docs/123/changes?since=' + lastTimestamp)
  const data = await changes.json()
  applyChanges(data)
}, 500) // Poll every 500ms

// Problems with this approach:
// 1. 500ms lag feels awful for collaborative editing
// 2. At 100ms polling: 10 req/user/sec × 1000 users = 10,000 req/sec baseline
//    before any actual work happens
// 3. Each poll costs a full HTTP round trip even when nothing changed
// 4. Under load, server queues fill, lag compounds

// WebSocket approach: zero polling, zero lag
const ws = new WebSocket('wss://api.example.com/docs/123')

ws.onmessage = (event) => {
  const change = JSON.parse(event.data)
  applyChange(change)  // Called the moment the server has it
}

// User A types → server broadcasts to all connected users immediately
// Server cost: one WebSocket send per change, not 10K polls/sec

At 1,000 concurrent users polling every 100ms, a REST server handles 10,000 requests per second at idle — generating no useful work. WebSocket eliminates idle traffic entirely: the server only transmits when there is a change. Figma, Miro, Google Docs, and Notion all use WebSocket-based real-time sync for this exact reason.

The Problem WebSocket Cannot Solve: Stateless Scaling

REST's statelessness is not a limitation — it is its most operationally valuable property. Each request carries everything the server needs (auth token, request body, query params). Any server instance can handle any request. Scale horizontally by adding instances behind a load balancer. Zero state synchronization required.

// REST scaling: trivially horizontal
//
//  Client A  ──┐
//  Client B  ──┤── Load Balancer ──── Server 1
//  Client C  ──┤                 └─── Server 2
//  Client D  ──┘                 └─── Server 3
//
// Any request can go to any server. No shared state.
// Add Server 4: zero configuration, starts handling requests immediately.

// WebSocket scaling: stateful connections require coordination
//
//  Client A ──── Server 1 ─── Redis Pub/Sub ─── Server 2 ──── Client C
//                              (message broker)
//
// Client A connects to Server 1. Connection is pinned (sticky sessions).
// If Client A sends a message meant for Client C (on Server 2):
//   Server 1 cannot directly reach Client C.
//   Server 1 publishes to Redis. Server 2 subscribes, receives, forwards to C.
//
// What happens when Server 1 crashes?
//   Client A's connection drops. Client must reconnect (with backoff).
//   If using sticky sessions, load balancer must redistribute Client A.
//
// Tools: Socket.io Redis adapter, Ably, Pusher, AWS API Gateway WebSocket API

According to Ably's WebSocket infrastructure report, teams that self-host WebSocket servers at scale universally encounter the same sequence: connection pinning, Redis pub/sub, Redis saturation, custom sharding. Most eventually migrate to a managed WebSocket service (Ably, Pusher, AWS API Gateway WebSockets). Budget for this operational complexity before choosing WebSocket.

WebSocket Authentication: The Browser Constraint

A production detail that catches teams off guard: the browser's WebSocket API does not allow custom headers on the initial upgrade request. You cannot do:

// ❌ This does NOT work in browsers (Node.js only):
const ws = new WebSocket('wss://api.example.com/ws', {
  headers: { Authorization: 'Bearer eyJhbGci...' }
})
// Browser WebSocket API ignores the options object

// ✅ Option 1: Token in query string (most common browser pattern)
const token = await getShortLivedToken() // expires in 60 seconds
const ws = new WebSocket(`wss://api.example.com/ws?token=${token}`)
// Server validates token on upgrade, rejects with 401 if invalid

// ✅ Option 2: Authenticate after connection via first message
const ws = new WebSocket('wss://api.example.com/ws')
ws.onopen = () => {
  ws.send(JSON.stringify({ type: 'auth', token: localStorage.getItem('jwt') }))
}
ws.onmessage = (event) => {
  const msg = JSON.parse(event.data)
  if (msg.type === 'auth_ok') {
    // Now subscribed and authorized
  }
}

// ✅ Option 3: Session cookie (same-origin only)
// Browser automatically sends cookies with WebSocket upgrade request
// Works if your WS server is on the same domain as your auth server
const ws = new WebSocket('wss://api.example.com/ws')
// Server reads the session cookie from the upgrade request headers

The token-in-query-string pattern is fine as long as tokens are short-lived (60–300 seconds). Avoid long-lived JWTs in URLs — they appear in server access logs. Use your JWT Decoder to verify token expiry and claims during development.

Server-Sent Events: The Overlooked Middle Ground

Before defaulting to WebSocket for any server-push requirement, evaluate Server-Sent Events (SSE). SSE is unidirectional (server → client only) but has significant operational advantages over WebSocket for the right use cases:

// SSE: server side (Node.js/Express)
app.get('/api/events', (req, res) => {
  res.setHeader('Content-Type', 'text/event-stream')
  res.setHeader('Cache-Control', 'no-cache')
  res.setHeader('Connection', 'keep-alive')

  // Send a heartbeat every 30s to keep connection alive
  const heartbeat = setInterval(() => {
    res.write(':heartbeat

')  // comment line, not an event
  }, 30000)

  // Push events whenever they occur
  const unsubscribe = eventBus.on('order-updated', (order) => {
    res.write(`event: order
`)
    res.write(`data: ${JSON.stringify(order)}

`)
  })

  req.on('close', () => {
    clearInterval(heartbeat)
    unsubscribe()
  })
})

// SSE: browser client — EventSource has built-in reconnection
const eventsource = new EventSource('/api/events')
eventsource.addEventListener('order', (event) => {
  const order = JSON.parse(event.data)
  updateOrderStatus(order)
})
// EventSource reconnects automatically after network interruptions
// Last-Event-ID header allows resuming from last received event

// Compare to WebSocket client reconnection — manual:
function connectWs() {
  const ws = new WebSocket('wss://api.example.com/events')
  ws.onclose = (event) => {
    if (!event.wasClean) {
      setTimeout(connectWs, Math.min(1000 * 2 ** retryCount++, 30000))
    }
  }
}

SSE advantages over WebSocket: works natively through HTTP/2 multiplexing (multiple SSE streams share one TCP connection), uses standard HTTP infrastructure (no special CORS, no upgrade), provides automatic reconnection with Last-Event-ID replay, and allows normal REST authentication via Authorization headers. OpenAI's streaming API uses SSE. GitHub's live activity feed uses SSE. If your server only needs to push — not receive — start with SSE.

Production WebSocket Implementation

A minimal but production-ready WebSocket server in Node.js with authentication, ping/pong heartbeat, and graceful cleanup:

import { WebSocketServer, WebSocket } from 'ws'
import http from 'http'
import { verifyToken } from './auth'

const server = http.createServer()
const wss = new WebSocketServer({ noServer: true })

// Map of userId → Set<WebSocket> for fan-out broadcasting
const connections = new Map<string, Set<WebSocket>>()

server.on('upgrade', async (request, socket, head) => {
  // Parse and validate token from query string
  const url = new URL(request.url!, 'wss://example.com')
  const token = url.searchParams.get('token')

  const user = token ? await verifyToken(token) : null
  if (!user) {
    socket.write('HTTP/1.1 401 Unauthorized

')
    socket.destroy()
    return
  }

  wss.handleUpgrade(request, socket, head, (ws) => {
    wss.emit('connection', ws, user)
  })
})

wss.on('connection', (ws, user) => {
  // Register connection
  if (!connections.has(user.id)) connections.set(user.id, new Set())
  connections.get(user.id)!.add(ws)

  // Heartbeat: detect stale connections (60s timeout)
  let isAlive = true
  ws.on('pong', () => { isAlive = true })
  const heartbeat = setInterval(() => {
    if (!isAlive) { ws.terminate(); return }
    isAlive = false
    ws.ping()
  }, 30000)

  ws.on('message', (data) => {
    // Handle incoming messages from this client
    const msg = JSON.parse(data.toString())
    handleMessage(ws, user, msg)
  })

  ws.on('close', () => {
    clearInterval(heartbeat)
    connections.get(user.id)?.delete(ws)
  })
})

// Broadcast to all connections for a user (multi-tab support)
function broadcastToUser(userId: string, payload: object) {
  const userConns = connections.get(userId)
  if (!userConns) return
  const message = JSON.stringify(payload)
  for (const ws of userConns) {
    if (ws.readyState === WebSocket.OPEN) ws.send(message)
  }
}

server.listen(3001)

The Hybrid Architecture: What Production Looks Like

The correct answer for most production applications in 2026 is not WebSocket versus REST — it is REST for everything except real-time event delivery, and WebSocket or SSE for event delivery.

  • Authentication: REST. POST /auth/login, receive JWT. WebSocket tokens derived from this.
  • Data fetching: REST. GET /api/orders, GET /api/user/profile. Benefits from HTTP caching, CDN, standard tooling.
  • Mutations: REST. POST /api/orders, PATCH /api/orders/123. HTTP semantics, idempotency keys, standard error codes.
  • Real-time events: WebSocket or SSE. "Order status changed", "new message arrived", "collaborator moved cursor". Server-initiated, low-latency.
  • Live data feeds: SSE if server-to-client only (stock prices, notifications, progress). WebSocket if bidirectional (trading execution, collaborative editing).

Companies like Slack use this exact split: REST API for loading channels, messages, and user data; WebSocket for receiving real-time message and presence updates. The REST surface covers 95% of the API, while WebSocket handles the 5% that requires push. Per Slack Engineering, this separation means their CDN handles the majority of API traffic (reads), while WebSocket servers handle only the event stream, which scales independently. Validate your REST responses with the JSON Formatter while building your API layer.

Protocol Comparison Summary

PropertyREST (HTTP)WebSocketServer-Sent Events
DirectionClient → ServerBidirectionalServer → Client
ConnectionPer request (HTTP/1.1) or multiplexed (HTTP/2)Persistent TCP socketPersistent HTTP stream
Server pushNo (polling required)YesYes (server only)
HTTP cachingFull (CDN, browser)NoneNone
Horizontal scalingTrivial (stateless)Complex (sticky + pubsub)Moderate (sticky sessions)
Auth headersAuthorization: Bearer (standard)Token in URL / post-connectAuthorization: Bearer (standard)
ReconnectionAutomatic (stateless)Manual (implement backoff)Automatic (EventSource)
Message overhead400–800 bytes/request2–14 bytes/messageHTTP stream (moderate)
Best forCRUD, data fetching, authChat, games, collab editingNotifications, feeds, streaming

Decision Framework: Which Protocol to Use

Use REST when:

  • The client initiates all interactions (request-response pattern)
  • Data changes infrequently and can be fetched on demand
  • CDN caching is important (product listings, documentation)
  • Third-party developers will consume the API (standard tooling advantage)
  • Operations must be idempotent with standard HTTP error semantics
  • Horizontal scaling simplicity is a priority

Use WebSocket when:

  • The server must push updates without a client request
  • Message frequency exceeds ~10/second per connection
  • Both server and client need to initiate messages (true bidirectional)
  • Use cases: chat, multiplayer games, collaborative editing, trading terminals
  • Latency is critical and polling delay is unacceptable

Use SSE when:

  • The server pushes to client only (no bidirectional needed)
  • Use cases: live notifications, order status updates, AI streaming responses, progress bars
  • You want automatic reconnection without custom client code
  • You need standard HTTP auth headers (not token-in-URL)
  • Infrastructure operates behind HTTP/2 proxies (SSE multiplexes cleanly)

Review the REST API design principles guide if you are building the REST layer, and the API authentication methods guide for securing both REST and WebSocket endpoints.

Debug Your API Responses

Whether you are building REST or WebSocket APIs, use BytePane's tools to inspect payloads, decode tokens, and validate data structures — all in your browser with no signup required.

Frequently Asked Questions

When should I use WebSocket instead of REST?

Use WebSocket when your application requires bidirectional, low-latency communication where the server must push updates without client requests. Ideal cases: chat applications, live scores, collaborative document editing, financial trading dashboards, and multiplayer games. REST cannot push — every update requires the client to poll.

Is WebSocket faster than REST?

For high-frequency exchange, yes — significantly. Per FeathersJS benchmarks, Socket.io handles ~4,000 messages/sec vs ~10 req/sec for HTTP REST, and 50 messages complete in ~180ms vs ~5 seconds over HTTP. For infrequent requests (one per second or less), REST over HTTP/2 is competitive and far simpler to operate.

Can WebSocket replace REST entirely?

No — teams that try this regret it. REST has structural advantages for HTTP caching, stateless horizontal scaling, standard tooling, and authentication flows. WebSocket connections are stateful, bypass CDN caches, are harder to load-balance, and require custom reconnection logic. The consensus is hybrid: REST for data, WebSocket for events.

How does WebSocket authentication work?

Browser WebSocket API cannot send custom headers on the upgrade request. Common patterns: (1) pass a short-lived token in the URL query string during handshake, (2) authenticate via a first message after connection opens, (3) use session cookies if on the same domain. Tokens in URLs must be short-lived (60–300s) to avoid log exposure.

What is the difference between WebSocket and Server-Sent Events?

SSE is unidirectional (server → client) over HTTP. WebSocket is bidirectional over persistent TCP. SSE advantages: standard HTTP auth headers, automatic EventSource reconnection, works through HTTP/2 multiplexing. Use SSE for one-way feeds (notifications, AI streaming). Use WebSocket when clients also need to send messages frequently.

How do you scale WebSocket servers?

WebSocket connections are stateful. Scale by: (1) sticky sessions at the load balancer so reconnections reach the same server, (2) Redis Pub/Sub to broadcast messages across server instances, (3) dedicated managed services (Ably, Pusher, AWS API Gateway WebSocket) to offload the complexity. Self-hosting at scale is non-trivial.

Related Articles