Infrastructure Cost Comparison: SSE vs WebSockets Permalink to this section

Part of SSE vs WebSockets vs HTTP Polling, under SSE Protocol Fundamentals & Architecture.

The two transports cost almost the same to run and noticeably different amounts to own. Building the model explicitly is worth an hour, because the intuition — “WebSockets are more efficient” — is true for bytes and irrelevant at the scale most feeds operate.

Symptom & Developer Intent Permalink to this section

  • A transport decision is being made on general impressions rather than on numbers.
  • The load balancer bill is dominated by connection hours and nobody predicted it.
  • A WebSocket deployment costs more in engineering time than in infrastructure.
  • Capacity planning assumes connection cost scales with message volume, and it does not.
  • A migration is proposed on efficiency grounds without an estimate of the saving.

The intent is a defensible cost model for a specific workload, covering the line items that actually move.

Root Cause Analysis Permalink to this section

Cost for a persistent-connection system has four components, and only one of them differs meaningfully between SSE and WebSockets.

Where the money actually goes Bar chart of the relative size of connection-hour, compute, egress and engineering cost for a persistent-connection feature. Where the money actually goes relative monthly cost, 10 000 concurrent clients Load balancer hours 100× Compute 45× Egress 30× Framing difference relative share of monthly cost
Framing overhead — the thing the transport argument is usually about — is the smallest bar on the chart.

Connection hours. Both transports hold one connection per client. Cloud load balancers bill per connection-hour or per capacity unit, and the charge is identical whichever protocol rides on it. Ten thousand concurrent clients for a month is the same connection-hour bill either way.

Compute per connection. Both cost one file descriptor and a slice of memory. WebSockets add frame parsing and masking on the receive path; SSE adds nothing on receive because there is no receive path. In practice both land in the tens of kilobytes per idle connection, and per-connection application state dominates either.

Egress. WebSocket framing is two to fourteen bytes per message; SSE framing is the field names plus newlines, typically twenty to forty bytes. On messages of a few hundred bytes that difference is single-digit percent of egress. It matters only for very small, very frequent messages — the case where WebSockets genuinely win.

Engineering time. This is the component that differs by a large factor and never appears on the invoice. A WebSocket client needs reconnection, backoff, heartbeat and resume logic; the browser implements all four for SSE. That is code to write, test, maintain across platforms, and debug at three in the morning.

Step-by-Step Resolution Permalink to this section

Cost line items, transport by transport Table comparing SSE and WebSockets across connection hours, compute per connection, framing overhead and client code to maintain. Cost line items, transport by transport SSE WebSocket Connection hours identical identical Compute per connection ~40 KB ~40 KB Framing per message ~32 B ~6 B Client code to own none reconnect + heartbeat + resume
Three of the four rows are identical. The one that differs is the one that never appears on an invoice.

Step 1 — Write down the workload Permalink to this section

// The four numbers everything else derives from.
const workload = {
  concurrentClients: 10_000,
  messagesPerClientPerMinute: 6,
  averagePayloadBytes: 400,
  hoursPerMonth: 730,
};

Step 2 — Compute egress for each transport Permalink to this section

const FRAME = { sse: 32, websocket: 6 };          // framing overhead per message, bytes

function monthlyEgressGB(w, transport) {
  const perMessage = w.averagePayloadBytes + FRAME[transport];
  const messages = w.concurrentClients * w.messagesPerClientPerMinute * 60 * w.hoursPerMonth;
  return (messages * perMessage) / 1e9;
}

// sse:       ~1 135 GB      websocket: ~1 067 GB      difference ≈ 6%

Add heartbeats for SSE — a 12-byte comment every 15 seconds across ten thousand clients is about 2 GB a month, well inside the rounding.

Step 3 — Compute connection-hour cost, which is transport-neutral Permalink to this section

// Cloud load balancers bill per connection-hour or per capacity unit; the
// protocol riding on the connection does not change the price.
const connectionHours = workload.concurrentClients * workload.hoursPerMonth;   // 7.3M

Both transports pay this identically. It is usually the largest infrastructure line item for a persistent-connection feature, and it is the one that surprises teams migrating from polling.

Step 4 — Compute compute capacity Permalink to this section

// Both transports: ~40 KB per idle connection plus application state.
const MEMORY_PER_CONN_KB = 40;
const nodesNeeded = Math.ceil(
  (workload.concurrentClients * MEMORY_PER_CONN_KB) / (4 * 1024 * 1024),   // 4 GB nodes
);
// 10 000 connections ≈ 400 MB — one node, with descriptor limits raised.

Descriptor limits, not memory, are the binding constraint here — see Tuning File-Descriptor Limits for SSE Connection Pools.

Step 5 — Price the engineering time honestly Permalink to this section

Client-side code a WebSocket feed needs and SSE does not:
  reconnection with backoff and jitter     ~120 lines, plus tests
  heartbeat / liveness detection            ~45 lines
  resume cursor and replay handling         ~60 lines
  per-platform reimplementation             × each client platform
  ongoing maintenance and incident time     recurring

Against roughly six percent of egress, this is where the real difference sits — and it points the same way for every feed that does not need client-to-server messages.

Step 6 — Decide on the workload’s shape, not on the transport’s reputation Permalink to this section

function recommend(w, needsClientToServer, binary) {
  if (needsClientToServer || binary) return 'WebSocket';
  if (w.messagesPerClientPerMinute > 600) return 'WebSocket — framing overhead starts to matter';
  return 'SSE — same infrastructure cost, materially less code to own';
}

Validation & Monitoring Permalink to this section

Validate the model against a real week rather than trusting the arithmetic.

What the model recommends for this workload Decision tree selecting a transport from data direction and message frequency once the cost model has been built. What the model recommends for this workload Does the client send messages? SSE same cost, less code no WebSocket duplex earns its keep yes WebSocket framing starts to matter no, but 600+/min
Framing efficiency only decides the answer when messages are both tiny and very frequent — and that workload usually needs a client-to-server channel anyway.
# Actual egress attributable to the stream path.
sum(rate(nginx_http_response_size_bytes_sum{location="/events"}[1h])) * 3600 * 730 / 1e9

# Actual concurrent connections, which drives the connection-hour bill.
sum(sse_streams_open)

# Framing share: bytes sent divided by payload bytes published.
sum(rate(sse_bytes_sent_total[1h])) / sum(rate(sse_payload_bytes_total[1h]))

If the framing share is close to one, framing is irrelevant to your bill and the transport decision should be made on other grounds. If it is above about 1.3, your messages are small enough that WebSocket framing is a genuine saving — and small, frequent messages usually also mean the client sends some, which points to WebSockets anyway.

Re-run the model when the workload shape changes rather than when the bill grows: a doubling of clients is a linear change, while a shift from six messages a minute to sixty is the kind of change that moves the recommendation.

Production Checklist Permalink to this section

Frequently Asked Questions Permalink to this section

Are WebSockets cheaper to run than SSE?

Marginally, on egress, because their framing is smaller — typically a few percent on payloads of a few hundred bytes. Connection hours and compute are effectively identical, and those dominate the bill. The saving only becomes material for very small, very frequent messages.

What is the largest cost line for a persistent-connection feature?

Usually load balancer connection hours, because you pay for every client for every hour they are connected regardless of traffic. It is also the line that surprises teams moving from polling, where connections were short-lived and the cost showed up as request volume instead.

Do heartbeats meaningfully increase SSE's cost?

No. A twelve-byte comment every fifteen seconds across ten thousand clients is a couple of gigabytes a month — a rounding error against payload traffic. Intervals below about ten seconds start to be measurable without buying additional safety.

How should engineering time enter the comparison?

As a real line item. Reconnection, backoff, heartbeat and resume are four pieces of code a WebSocket client owns per platform and an SSE client gets from the browser. At typical engineering rates that difference outweighs a single-digit percentage of egress for most feeds.