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.
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
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.
# 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.