WebSockets vs HTTP

When to use real-time communication — persistent connections, code examples, and scaling strategies

API DevelopmentJuly 1, 202616 min readBy Keyur Patel

Picture this: you're building a chat app. Your first instinct is to poll the server every second — hit an endpoint, check for new messages, repeat. It works. But now you have 10,000 users making an HTTP request every second. That's 10,000 requests per second just to check "anything new?" — and 95% of the time the answer is "nope."

That's the exact problem WebSockets solve. Instead of constantly asking "got anything?", you open a persistent connection and the server tells you the moment something happens. No wasted requests, no added latency, no hammering your infrastructure.

But here's the thing — WebSockets aren't always the answer. Most web apps do perfectly fine with plain HTTP. This guide breaks down exactly when each protocol makes sense, how they differ under the hood, and how to implement both correctly.

What Is HTTP? The Request-Response Model

HTTP (HyperText Transfer Protocol) is the foundation of web communication. It's a stateless, request-response protocol: the client sends a request, the server processes it, sends back a response, and the connection is done.

Every time you load a web page, submit a form, or call a REST API — that's HTTP. One request in, one response out. The server has no memory of previous requests unless you explicitly pass state via cookies, tokens, or session IDs.

HTTP Request-Response Flow:

Client                          Server
  |                               |
  |--- GET /api/messages -------->|  (1) Client initiates request
  |                               |  (2) Server processes
  |<-- 200 OK + JSON response ----|  (3) Server responds
  |                               |  (4) Connection closes
  |                               |
  |--- GET /api/messages -------->|  (5) Client asks again later
  |<-- 200 OK + JSON response ----|  (6) New connection, new response
  |                               |

Key characteristics of HTTP:

  • Stateless — each request is independent, server doesn't remember previous ones
  • Unidirectional — client always initiates, server only responds
  • One-shot — connection opens, data transfers, connection closes
  • Cacheable — responses can be cached by browsers, CDNs, and proxies
  • Universal — works everywhere, every language, every platform

HTTP/1.1 introduced keep-alive connections (reusing TCP connections for multiple requests), and HTTP/2 added multiplexing (multiple requests over a single connection). But the fundamental model is the same: client asks, server answers.

What Are WebSockets? Persistent Bidirectional Communication

WebSockets (RFC 6455) provide a persistent, full-duplex communication channel over a single TCP connection. Once established, both the client and server can send data to each other at any time — no waiting for the other side to ask first.

Think of HTTP as sending letters back and forth. WebSockets are like a phone call — once connected, either party can speak at any time without dialing again.

WebSocket Connection Flow:

Client                          Server
  |                               |
  |--- HTTP Upgrade Request ----->|  (1) Starts as HTTP
  |<-- 101 Switching Protocols ---|  (2) Server agrees to upgrade
  |                               |
  |======= WebSocket Open ========|  (3) Persistent connection established
  |                               |
  |--- "Hello" ------------------>|  (4) Client sends anytime
  |<-- "Hi there!" ---------------|  (5) Server sends anytime
  |<-- "New notification" --------|  (6) Server pushes without request
  |--- "typing..." -------------->|  (7) Client sends without waiting
  |<-- "User joined" -------------|  (8) Server pushes event
  |                               |
  |======= Connection Close ======|  (9) Either side can close

Key characteristics of WebSockets:

  • Persistent — connection stays open until explicitly closed
  • Bidirectional — both client and server can initiate messages
  • Low overhead — after handshake, messages are just 2-6 bytes of framing
  • Real-time — data arrives instantly, no polling delay
  • Event-driven — server pushes data when events happen

WebSockets use the ws:// protocol (or wss:// for encrypted connections, equivalent to HTTPS). The connection starts as an HTTP request and "upgrades" to the WebSocket protocol via the handshake.

How the WebSocket Handshake Works

Every WebSocket connection starts life as a regular HTTP request. The client sends a special "upgrade" request, and if the server supports WebSockets, it responds with HTTP 101 (Switching Protocols). After that, the connection is no longer HTTP — it's a raw WebSocket connection.

Client Upgrade Request:

GET /chat HTTP/1.1
Host: server.example.com
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
Sec-WebSocket-Version: 13
Origin: http://example.com

Server Upgrade Response:

HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=

What happens during the handshake:

  1. Client sends a GET request with Upgrade: websocket header
  2. Server validates the request and checks origin, protocols, and authentication
  3. Server responds with 101 status code and Sec-WebSocket-Accept (hashed key for verification)
  4. Both sides switch from HTTP to the WebSocket protocol
  5. The TCP connection remains open for bidirectional framed messages

This design is clever — the handshake uses standard HTTP so it works through existing infrastructure (load balancers, proxies, firewalls). Once upgraded, the connection becomes a lightweight binary protocol with minimal framing overhead.

HTTP vs WebSocket: Side-by-Side Comparison

Here's a comprehensive comparison of both protocols across every dimension that matters for your architecture decisions:

FeatureHTTPWebSocket
Connection modelShort-lived — opens and closes per requestPersistent — stays open until explicitly closed
Communication directionUnidirectional — client initiates onlyBidirectional — both sides can send anytime
Per-message overheadLarge — full HTTP headers (200-800 bytes) every requestTiny — 2-6 bytes framing per message
LatencyHigher — connection setup + headers per requestUltra-low — data sent instantly over open connection
Scalability modelStateless — easy horizontal scaling, load balancers trivialStateful — requires sticky sessions, pub/sub coordination
CachingBuilt-in — browsers, CDNs, proxies all cache HTTP nativelyNone — no caching mechanism in the protocol
Protocolhttp:// or https:// (port 80/443)ws:// or wss:// (port 80/443)
State managementStateless — pass state via headers/cookies/tokensStateful — server tracks each connection in memory
Browser supportUniversal — every browser, every deviceUniversal — all modern browsers (IE10+)
Proxy/firewall compatibilityExcellent — standard web traffic passes through everythingGood — starts as HTTP so most proxies allow; some corporate firewalls may block
Best forREST APIs, page loads, form submissions, CRUD operationsChat, gaming, live feeds, collaborative editing, trading
Error handlingHTTP status codes (404, 500, etc.)Close frames with codes (1000, 1001, etc.)

The key insight: HTTP is optimized for stateless, cacheable request-response patterns. WebSockets are optimized for stateful, real-time, bidirectional streaming. Neither is "better" — they solve fundamentally different communication patterns.

When to Use HTTP (Most of the Time)

HTTP should be your default. Most web applications don't need real-time communication, and HTTP gives you simplicity, cacheability, and effortless scaling. Use HTTP when:

  • REST APIs — CRUD operations on resources (create user, get product, update order)
  • Static content — serving pages, images, CSS, JavaScript files
  • Form submissions — login, registration, checkout, contact forms
  • Data that changes infrequently — user profile, settings, product catalogs
  • Public APIs — third-party integrations benefit from HTTP's universality and caching
  • Search and filtering — request → response fits perfectly
  • File uploads and downloads — HTTP handles multipart data natively
  • Anything that benefits from caching — CDNs, browser cache, proxy cache all speak HTTP

Rule of thumb: If the user triggers the data fetch (clicking a button, loading a page, submitting a form), HTTP is almost certainly the right choice. WebSockets only make sense when data needs to arrive without the user asking for it.

When to Use WebSockets (Real-Time Only)

WebSockets shine when you need instant data delivery without the client constantly asking "is there something new?" Use WebSockets for:

  • Chat applications — instant message delivery, typing indicators, read receipts, presence
  • Multiplayer gaming — player positions, game state updates, real-time physics sync
  • Live notifications — push alerts the instant something happens (new order, mention, deployment status)
  • Collaborative editing — Google Docs-style real-time cursors, conflict resolution, live typing
  • Financial trading — stock tickers, order book updates, price feeds at sub-second intervals
  • Live dashboards — server metrics, analytics, IoT sensor data streaming in real-time
  • Live sports/events — score updates, play-by-play, auction bidding
  • Video/audio signaling — WebRTC coordination (though media travels via UDP)

The real test: Does the server need to push data to the client without waiting for a request? Does the data need to arrive in under 100ms? Are there frequent, small updates? If yes to any of these — WebSockets are worth the added complexity.

Code Examples: WebSocket Client & Server

Let's build a working WebSocket chat system. I'll show you both the client-side JavaScript and the Node.js server using the popular ws library.

WebSocket Server (Node.js with ws)

const WebSocket = require("ws")
const http = require("http")

// Create HTTP server (WebSocket piggybacks on this)
const server = http.createServer((req, res) => {
  res.writeHead(200, { "Content-Type": "text/plain" })
  res.end("WebSocket server running")
})

// Create WebSocket server
const wss = new WebSocket.Server({ server })

// Track connected clients
const clients = new Set()

wss.on("connection", (ws, req) => {
  const clientId = req.headers["sec-websocket-key"].slice(0, 8)
  clients.add(ws)
  console.log(`Client connected: ${clientId} (total: ${clients.size})`)

  // Send welcome message
  ws.send(JSON.stringify({
    type: "system",
    message: "Connected to chat server",
    users: clients.size
  }))

  // Handle incoming messages
  ws.on("message", (data) => {
    const message = JSON.parse(data)
    console.log(`Received from ${clientId}: ${message.text}`)

    // Broadcast to all other connected clients
    for (const client of clients) {
      if (client !== ws && client.readyState === WebSocket.OPEN) {
        client.send(JSON.stringify({
          type: "message",
          from: clientId,
          text: message.text,
          timestamp: Date.now()
        }))
      }
    }
  })

  // Handle disconnection
  ws.on("close", (code, reason) => {
    clients.delete(ws)
    console.log(`Client disconnected: ${clientId} (code: ${code})`)
  })

  // Handle errors
  ws.on("error", (error) => {
    console.error(`WebSocket error for ${clientId}:`, error.message)
    clients.delete(ws)
  })
})

// Heartbeat to detect dead connections
setInterval(() => {
  wss.clients.forEach((ws) => {
    if (ws.isAlive === false) return ws.terminate()
    ws.isAlive = false
    ws.ping()
  })
}, 30000)

wss.on("connection", (ws) => {
  ws.isAlive = true
  ws.on("pong", () => { ws.isAlive = true })
})

server.listen(8080, () => {
  console.log("Server running on ws://localhost:8080")
})

WebSocket Client (Browser JavaScript)

// Connect to WebSocket server
const ws = new WebSocket("wss://your-server.com/chat")

// Connection opened
ws.addEventListener("open", () => {
  console.log("Connected to server")
  
  // Send a message
  ws.send(JSON.stringify({
    type: "message",
    text: "Hello everyone!"
  }))
})

// Listen for messages from server
ws.addEventListener("message", (event) => {
  const data = JSON.parse(event.data)
  
  switch (data.type) {
    case "message":
      displayMessage(data.from, data.text, data.timestamp)
      break
    case "system":
      displaySystemMessage(data.message)
      break
    case "typing":
      showTypingIndicator(data.from)
      break
  }
})

// Handle connection close
ws.addEventListener("close", (event) => {
  console.log(`Disconnected: code=${event.code}, reason=${event.reason}`)
  
  // Reconnect with exponential backoff
  setTimeout(() => reconnect(), getBackoffDelay())
})

// Handle errors
ws.addEventListener("error", (error) => {
  console.error("WebSocket error:", error)
})

// Reconnection logic with exponential backoff
let reconnectAttempts = 0

function getBackoffDelay() {
  const delay = Math.min(1000 * Math.pow(2, reconnectAttempts), 30000)
  reconnectAttempts++
  return delay
}

function reconnect() {
  console.log(`Reconnecting (attempt ${reconnectAttempts})...`)
  // Re-create the WebSocket connection
  // Reset reconnectAttempts on successful connection
}

HTTP Polling vs WebSocket: A Direct Comparison

The most common alternative to WebSockets is HTTP polling — repeatedly hitting an endpoint to check for updates. Let's compare both approaches for a notification system:

Approach 1: HTTP Short Polling

// HTTP Polling — check every 3 seconds
async function pollNotifications() {
  while (true) {
    try {
      const res = await fetch("/api/notifications?since=" + lastTimestamp)
      const data = await res.json()
      
      if (data.notifications.length > 0) {
        data.notifications.forEach(showNotification)
        lastTimestamp = data.notifications.at(-1).timestamp
      }
    } catch (err) {
      console.error("Poll failed:", err)
    }
    
    // Wait 3 seconds before next check
    await new Promise(r => setTimeout(r, 3000))
  }
}

// Problems with this approach:
// - 3-second delay before user sees notification
// - Server handles requests even when there's nothing new
// - 28,800 requests per user per day (even if nothing happens)
// - Each request carries full HTTP headers (~500 bytes overhead)
// - Scaling: 10,000 users = 3,333 requests/second for NOTHING

Approach 2: WebSocket (Push)

// WebSocket — server pushes instantly
const ws = new WebSocket("wss://api.example.com/notifications")

ws.addEventListener("message", (event) => {
  const notification = JSON.parse(event.data)
  showNotification(notification) // Instant delivery!
})

// Benefits of this approach:
// - Zero delay — notification arrives the instant it's created
// - No wasted requests — server only sends when there's data
// - 1 connection per user (not 28,800 requests/day)
// - ~6 bytes overhead per message (vs ~500 bytes HTTP headers)
// - Scaling: 10,000 users = 10,000 open connections (idle = cheap)
MetricHTTP Polling (3s)WebSocket
Latency to receive update0–3 seconds (average 1.5s)< 50ms (instant)
Requests/day (per user)~28,8001 (connection stays open)
Bandwidth overhead~14 MB/day headers alone~100 bytes/day (heartbeats)
Server load (10k users)3,333 req/sec continuously10k idle connections (minimal CPU)
Battery impact (mobile)High — constant network activityLow — idle connection uses minimal power
ComplexitySimple — standard HTTP fetchModerate — reconnection, heartbeats, error handling

For a small internal tool with 50 users, polling every 5 seconds is perfectly fine. For a consumer-facing app with thousands of concurrent users expecting instant updates — WebSockets save you enormous bandwidth and deliver a much better user experience.

Scaling WebSockets: The Hard Parts

HTTP is easy to scale — throw more servers behind a load balancer and requests get distributed evenly. WebSockets are harder because connections are stateful. A user connected to Server A can't receive messages broadcast from Server B unless you build coordination between them.

Challenge 1: Sticky Sessions

Once a WebSocket connection is established with a specific server, all subsequent communication must go through that server. If a client reconnects (after a network blip) and hits a different server, they lose their state. Solutions:

  • Configure load balancer sticky sessions (based on IP or cookie)
  • Use connection-aware routing at the load balancer level
  • Store connection state externally (Redis) so any server can resume

Challenge 2: Cross-Server Messaging (Pub/Sub)

User A is connected to Server 1, User B to Server 2. When A sends a message to B, Server 1 needs to tell Server 2 to deliver it. This requires a message broker:

// Redis Pub/Sub for cross-server WebSocket messaging
const Redis = require("ioredis")
const pub = new Redis()  // Publisher
const sub = new Redis()  // Subscriber

// When a message comes in via WebSocket on THIS server:
wss.on("connection", (ws) => {
  ws.on("message", (data) => {
    const msg = JSON.parse(data)
    
    // Publish to Redis — ALL servers will receive this
    pub.publish("chat:" + msg.room, JSON.stringify({
      from: msg.userId,
      text: msg.text,
      timestamp: Date.now()
    }))
  })
})

// Subscribe to messages from ALL servers:
sub.subscribe("chat:*")
sub.on("message", (channel, message) => {
  const room = channel.split(":")[1]
  const data = JSON.parse(message)
  
  // Deliver to local clients in this room
  localClients.get(room)?.forEach((ws) => {
    if (ws.readyState === WebSocket.OPEN) {
      ws.send(message)
    }
  })
})

Challenge 3: Horizontal Scaling

Each server holds thousands of connections in memory. Scaling means tracking which users are on which servers. Common architectures:

  • Redis Pub/Sub — simple and effective for moderate scale (Socket.IO adapter)
  • NATS / RabbitMQ — more robust message routing for high-throughput
  • Kafka — ordered event streaming for very large scale with replay
  • Dedicated services — Ably, Pusher, or AWS API Gateway WebSocket handle scaling for you

Challenge 4: Connection Limits

Each open WebSocket consumes a file descriptor and memory. A single server typically handles 10k-100k connections. Beyond that:

  • Increase OS file descriptor limits (ulimit -n)
  • Use lightweight runtimes (Node.js, Go) that handle concurrency efficiently
  • Implement connection pooling and graceful shedding under load
  • Monitor memory usage per connection (each client's buffer state)

Common Mistakes When Using WebSockets

I've seen these mistakes repeatedly in production WebSocket implementations. Avoid them early and save yourself debugging sessions at 2 AM.

1. No reconnection logic

Connections will drop — mobile networks switch, servers deploy, firewalls timeout idle connections. Without automatic reconnection with exponential backoff, your app silently stops receiving updates and the user never knows.

2. Using WebSockets for everything

Fetching a user profile? Loading a product list? Use HTTP. WebSockets add statefulness, complexity, and lose caching. Only use them for features that genuinely need real-time push. A REST API + WebSocket for notifications is the right pattern for most apps.

3. No heartbeat/ping-pong mechanism

Without heartbeats, you can't detect dead connections. A client's network drops and the server still holds the connection open, wasting resources. Implement ping/pong frames every 30 seconds to detect and clean up stale connections.

4. Sending unstructured messages

Sending raw strings like "hello" with no type field or structure makes the system impossible to extend. Always use a message format with type, payload, and metadata: {"type": "chat", "payload": {...}}

5. No authentication on WebSocket connections

Anyone can open a WebSocket connection to your server. Validate auth tokens during the initial HTTP handshake (via query params or cookies) and reject unauthorized connections before upgrading to WebSocket.

6. Not handling backpressure

If the server sends data faster than the client can consume it, the buffer grows and memory explodes. Monitor bufferedAmount on the client side and implement flow control on the server side — pause sending if the client falls behind.

7. Ignoring message ordering and delivery guarantees

TCP guarantees ordering within a single connection, but if a client reconnects to a different server, they may miss messages sent during the gap. Implement message sequencing (IDs or timestamps) and allow clients to request missed messages on reconnect.

WebSocket Best Practices

If you're implementing WebSockets in production, here are the practices that separate reliable systems from ones that break under real-world conditions:

1. Always use WSS (WebSocket Secure)

Use wss:// in production, never plain ws://. It encrypts traffic, prevents man-in-the-middle attacks, and avoids proxy interference. Most corporate proxies terminate unencrypted WebSocket connections.

2. Implement exponential backoff for reconnection

Don't reconnect immediately or at a fixed interval. Use exponential backoff (1s, 2s, 4s, 8s... up to 30s max) with jitter. This prevents thundering herd problems when a server restarts and thousands of clients try to reconnect simultaneously.

3. Define a clear message protocol

Design your message format upfront. Include type, version, payload, and correlation ID. This lets you evolve the protocol without breaking existing clients. Consider using Protocol Buffers or MessagePack for binary efficiency at scale.

4. Authenticate during the handshake

Pass JWT or session token via query parameter or cookie during the upgrade request. Validate it before accepting the connection. Re-validate periodically for long-lived connections (tokens expire).

5. Implement server-side heartbeats

Send ping frames every 30 seconds from the server. If a client doesn't respond with pong within 10 seconds, terminate the connection. This detects zombie connections and reclaims resources.

6. Use rooms/channels for message routing

Don't broadcast everything to everyone. Organize connections into rooms or channels (chat rooms, user-specific channels, topic subscriptions). Only send messages to clients who actually need them.

7. Rate-limit incoming messages

A malicious or buggy client can flood your server with messages. Implement per-connection rate limiting (e.g., max 100 messages/second). Disconnect clients that exceed the limit.

8. Monitor connection metrics

Track total active connections, message throughput, connection duration, reconnection rate, and error rates. Set alerts for abnormal patterns — a spike in disconnections often signals infrastructure issues.

9. Handle graceful shutdown

When deploying, send close frames with a "server restarting" reason code before shutting down. Clients receive this and reconnect cleanly rather than experiencing a broken pipe error. Drain connections over 10-30 seconds during deploys.

10. Have a fallback strategy

Some environments block WebSockets (corporate networks, certain mobile carriers). Have a fallback to long polling or SSE. Libraries like Socket.IO handle this automatically with transport negotiation.

Frequently Asked Questions

What is the main difference between WebSockets and HTTP?
HTTP is a request-response protocol where the client initiates every interaction and the connection closes after each exchange. WebSockets maintain a persistent, bidirectional connection where both client and server can send data at any time without waiting for the other side to initiate.
Are WebSockets faster than HTTP?
For real-time data, yes. WebSockets eliminate the overhead of establishing new connections and sending HTTP headers with every message. Once connected, messages are just 2-6 bytes of framing overhead vs hundreds of bytes for HTTP headers. But for one-shot requests like loading a web page, HTTP is perfectly efficient.
Can WebSockets replace HTTP entirely?
No. WebSockets are designed for real-time bidirectional communication. They lack HTTP features like caching, CDN support, built-in compression negotiation, standard status codes, and browser navigation. Use HTTP for standard web traffic and WebSockets specifically for real-time features.
Do WebSockets work through firewalls and proxies?
Usually yes, because the WebSocket handshake starts as a standard HTTP request on port 80/443. However, some corporate proxies and firewalls may terminate long-lived connections or block the upgrade. Using WSS (WebSocket Secure over TLS) on port 443 resolves most proxy issues.
How many WebSocket connections can a server handle?
A well-configured server can handle 50,000-100,000+ concurrent WebSocket connections. The bottleneck is usually RAM (each connection uses memory for its buffer) and file descriptors rather than CPU. Node.js and Go are particularly good at handling many concurrent connections.
Should I use WebSockets for a chat application?
Yes, WebSockets are ideal for chat. They provide instant message delivery without polling, low latency, bidirectional communication (both sending and receiving), and presence indicators (typing, online status). All major chat apps use WebSockets or similar persistent connection protocols.
What is the difference between WebSockets and Server-Sent Events (SSE)?
Server-Sent Events (SSE) are unidirectional — only the server pushes data to the client over a standard HTTP connection. WebSockets are bidirectional — both sides can send data. SSE is simpler for scenarios like live feeds or notifications where the client only needs to receive updates.
How do you scale WebSockets horizontally?
Use sticky sessions (so reconnections hit the same server), a pub/sub layer like Redis to broadcast messages across servers, and a load balancer that supports WebSocket upgrades. Tools like Socket.IO with Redis adapter or dedicated services like Ably/Pusher handle this complexity for you.

Related Articles & Tools

Conclusion

The decision between HTTP and WebSockets isn't about which is "better" — it's about matching the communication pattern to your actual requirements. HTTP handles 90% of web communication perfectly: loading pages, calling APIs, submitting forms, fetching data. It's simple, cacheable, and scales effortlessly.

WebSockets fill a specific gap: when data needs to flow from server to client in real-time, without the client constantly asking for it. Chat, live collaboration, gaming, trading dashboards — these are the use cases that justify the added complexity of persistent connections and stateful servers.

My recommendation: start with HTTP for everything. When you hit a feature where polling feels wasteful and users are noticing the latency — that's when you bring in WebSockets for that specific feature. Not for your entire architecture, just for the parts that genuinely need real-time communication.