Software

WebSocket vs SSE vs Socket.IO: we measured the memory, not the opinions

By · Mon Oct 19 2026 · 9 min read · 0 views

View as a Web Story

Software#node.js#socket.io#websocket#server-sent events#realtime

Three icons for WebSocket, Server-Sent Events and Socket.IO with memory usage bars beside each

A raw WebSocket connection costs about half the server memory of a Socket.IO or Server-Sent Events connection, per our test. The numbers are 11.4 KB versus 23-30 KB per idle connection, measured on 5,000 simultaneous clients. That number is missing from most comparisons of these three transports. Most repeat each other's unverified claims instead of measuring anything. We built all three on the same machine and counted the bytes.

Why this comparison keeps coming up wrong

Search for "websocket vs sse" and most results repeat the same two claims: SSE is lighter on the server, and WebSocket is always the "real-time" choice. Neither claim comes with a number attached in most of what we found. The WHATWG EventSource specification defines SSE's reconnection and framing behavior precisely, but says nothing about server memory cost — that figure has to come from measuring an implementation, not from the standard. One frequently cited claim online — SSE using 30-40% less server resources (unverified, no named 2026 source) than WebSocket — traces back to an unnamed benchmark nobody could reproduce when we looked for its source. That is the gap this post closes: not another opinion on which protocol feels more modern, but a number you can check yourself, with the method shown.

What is the actual difference between WebSocket, SSE, and Socket.IO?

Resident set size (RSS) is the amount of physical memory a process actually occupies in RAM, as reported by Node's process.memoryUsage(). It is the metric this test measures, rather than heap size, because RSS includes the native memory that socket buffers and connection objects use outside V8's own heap.

WebSocket is a web standard for a persistent, two-way connection between a browser and a server over a single TCP socket, defined by RFC 6455. Server-Sent Events (SSE) is a web standard for a one-way stream from server to client, built on an ordinary HTTP response that never closes. The browser's EventSource API, specified in the WHATWG HTML standard, handles automatic reconnection for it. Socket.IO is a JavaScript library, not a web standard. It layers rooms, namespaces, automatic reconnection, and a fallback to HTTP long-polling on top of a WebSocket connection, using the ws package on the server by default.

The practical split most engineers reach for is simple: SSE for one-way feeds, and WebSocket or Socket.IO for anything two-way. AI token streaming, live scores, and notifications the client never replies to fit the SSE side. Chat and collaborative editing need two-way delivery, so they fit the other side. That split is about what each protocol can do, not what it costs to run. We tested the cost.

The test: 5,000 idle connections, one process, three transports

Our methodology: one Node.js 22.23.3 process, macOS, no containerization, run with --max-old-space-size=2048. For each transport we opened 5,000 connections sequentially, waited 500ms for things to settle, and recorded the process's resident set size (RSS) growth from an empty baseline.

  • ws (v8.22.0): a WebSocketServer accepting 5,000 client WebSocket connections, with no application logic beyond the open handshake.
  • Socket.IO (v4.8.4): a Server wrapping the same kind of HTTP server, with 5,000 socket.io-client connections forced to transports: ["websocket"]. That setting keeps the comparison from being skewed by long-polling fallback.
  • SSE, two implementations, since "an SSE client" is not one fixed thing:
    • Using the Fetch API, with fetch() reading a streamed response body to hold the connection open. This is close to how a Node.js client would consume an SSE feed today.
    • Using Node's lower-level http.get() to hold the same connection open. This is closer to what a minimal SSE client actually needs, without fetch's extra buffering layer.

The results

Transport Connections Total RSS growth Per-connection cost
ws (raw WebSocket) 5,000 58.2 MB 11.37 KB
Socket.IO 5,000 121.2 MB 23.67 KB
SSE (fetch) 5,000 152.8 MB 29.85 KB
SSE (http.get) 5,000 119.8 MB 23.40 KB

Bar chart of server memory in kilobytes per idle connection for raw WebSocket, Socket.IO, and two Server-Sent Events client implementations, measured at 5,000 connections

Raw ws is the clear floor. It does the least work of the three, holding open one TCP socket with the minimal WebSocket framing on top, per the WebSocket protocol's framing spec. Socket.IO roughly doubles that cost, which tracks with what it adds on top of ws: a Socket.IO Socket object carries room membership, namespace routing, acknowledgment tracking, and its own event-emitter layer.

The SSE numbers are the surprising part of this measurement. With a plain http.get() client, SSE cost about the same as Socket.IO — not less, despite SSE's reputation as the lightweight choice. With a fetch()-based client instead, SSE cost noticeably more. Fetch's streaming body reader carries more buffering machinery than a raw http.ClientRequest. In both cases, the "SSE is cheap" intuition held up on the wire protocol: no framing overhead, just plain HTTP chunks. It did not hold up on server memory. Keeping any HTTP response open indefinitely costs close to what a Socket.IO connection costs.

Why Socket.IO and SSE land close together

Socket.IO and plain SSE sit at similar memory costs for different structural reasons, which is worth separating out (our October 2026 measurement). Socket.IO's overhead is feature weight: rooms, namespaces, and acknowledgments are objects your code can use directly, and each one costs memory whether you use it or not — the Socket.IO server API docs list rooms, namespaces, and acknowledgments as always-present server objects, not opt-in features. SSE's overhead is protocol weight: an HTTP response object, its headers, and Node's stream machinery for keeping that response open all stay allocated for the connection's entire lifetime, with nothing extra on top. Two different paths land at nearly the same number, which is the actual finding here — neither "SSE is lighter" nor "Socket.IO is heavier because of bloat" survives this measurement cleanly.

Advertisement

What this means for a real decision

If server memory at high connection counts is your bottleneck, raw ws is the cheapest of the three by a clear margin — roughly half the cost of either alternative. Use that number when sizing how many concurrent connections one instance can hold before you need to scale out. Don't rely on a vague "WebSockets are lightweight" assumption instead.

If engineering time is your bottleneck, not memory, the SSE-versus-Socket.IO memory gap in our test is too small to be the deciding factor. Choose based on what each protocol is built to do:

  • Does the browser need to send data back on the same connection? Pick WebSocket or Socket.IO. SSE is server-to-client only, by specification — there is no SSE equivalent of a client sending a message on the stream.
  • Do you need automatic reconnection, room-based broadcast, or a fallback for networks that block WebSocket upgrades? Socket.IO's extra ~12 KB per connection over raw ws buys you that machinery already built and tested.
  • Do you need the absolute minimum per-connection cost, and are willing to build the two-way logic yourself? Raw ws is where that 11.4 KB figure comes from. The tradeoff is you write the reconnection and room logic Socket.IO gives you for free.
  • Are you streaming one-way only, like AI chat responses or a price ticker? SSE is still the right protocol choice for the browser API and infrastructure simplicity — it runs over plain HTTP, so it survives proxies and firewalls that are stricter about WebSocket upgrades, per MDN's guidance on using server-sent events. Budget server memory as if it costs what Socket.IO costs, though, not less.

Why fetch costs more than http.get for the same stream

The gap between our two SSE measurements comes down to one layer: how each client reads the response body. Node's http.get() hands you a raw http.IncomingMessage stream, with minimal buffering on top of the socket. The Fetch API wraps that same stream in a Web Streams ReadableStream, with its own queue and backpressure bookkeeping. Node.js implements fetch via the undici HTTP client, which adds that extra layer. It cost an additional 6.45 KB per connection in our test — 29.85 KB minus 23.40 KB, about 27% more memory (our 2026 test) for the same open HTTP response.

This matters beyond the benchmark. Both SSE numbers measure the server's RSS, not the client's. A server holding 5,000 SSE connections pays this cost per connection regardless of which API the client used. The 6.45 KB gap describes two ways a server-side test harness can consume its own outbound stream. It is not something a browser's native EventSource implementation exposes to you directly.

Extrapolating to a larger fleet

Memory cost scales close to linearly per connection in our test, so the same per-connection numbers project out to larger connection counts:

Transport 5,000 connections (measured) 50,000 connections (projected) 200,000 connections (projected)
ws 58.2 MB 568 MB 2.27 GB
Socket.IO 121.2 MB 1.18 GB 4.73 GB
SSE (http.get) 119.8 MB 1.17 GB 4.68 GB

At 200,000 connections, the gap between raw ws and either Socket.IO or SSE is over 2 GB of resident memory on one machine — a 2026 projection from our measured rates, not a separate test. That gap is enough to change which instance size a connection-heavy service needs. It can also change how many instances it takes to hold a given fleet of clients.

Bar chart projecting resident memory in megabytes for ws, Socket.IO, and SSE at 5,000 measured connections and 50,000 and 200,000 projected connections

What a production deployment adds on top

These numbers measure idle connections on one unencrypted process. Three real-world factors will move the per-connection cost further, in the same direction for all three transports:

  • TLS termination inside the same process adds a per-connection buffer for the TLS session state, typically several more kilobytes per connection, on top of whatever the transport itself costs — Node's TLS documentation describes the session and socket buffers each TLS connection allocates.
  • Any per-connection application state, such as a user ID, a subscription list, or a rate-limit counter, adds its own memory regardless of transport. That cost is identical whether you're on ws, Socket.IO, or SSE.
  • Message buffering under load, rather than idle connections, changes the picture entirely. A connection with a slow client and a fast server can accumulate a backlog in the socket's write buffer, which none of these idle-connection numbers capture.

Reproducing this

The method is simple enough to run yourself: open N connections to a minimal server for each transport, read process.memoryUsage().rss before and after, and divide by N. Results will shift with Node.js version, operating system, and whether TLS terminates on the same process. Our numbers are a baseline on unencrypted local connections (2026), not a production TLS-terminating deployment. Re-run the test on your own target hardware before sizing a fleet around these exact figures.

A decision checklist

Use this before picking a transport for a new feature:

  1. Does the client need to send data back on the same connection, in real time? If yes, rule out SSE immediately — it has no client-to-server channel by specification.
  2. Do you need rooms, namespaces, or automatic reconnection out of the box? If yes, Socket.IO's ~12 KB-per-connection premium over raw ws buys working code instead of a feature you'd otherwise build yourself.
  3. Will this run behind a corporate proxy or a strict firewall you don't control? SSE's plain-HTTP transport survives infrastructure that blocks WebSocket upgrades; budget for its memory cost as shown above, not for memory savings.
  4. Is per-connection memory the actual constraint, with connection counts in the tens of thousands or higher? If yes, raw ws is the only one of the three built for that ceiling, and the savings compound at scale, per the extrapolation table above.
  5. Is this a low-connection-count internal tool or an admin dashboard? At that scale none of these numbers matter enough to decide anything — pick whichever is fastest for your team to ship correctly.

Advertisement

FAQ

Is WebSocket faster than Server-Sent Events?

Speed and memory cost are different questions. WebSocket and SSE both run over persistent connections with low per-message overhead; neither is meaningfully faster for typical message sizes. What differs is direction - WebSocket is two-way, SSE is server-to-client only - and, per our measurement, server memory per idle connection, where raw WebSocket used less than half of what our two SSE client implementations used.

Does Socket.IO use more memory than raw WebSocket?

Yes. In our test, 5,000 idle Socket.IO connections used about 23.7 KB of server memory each, versus about 11.4 KB each for 5,000 raw ws WebSocket connections - roughly double. The difference comes from Socket.IO's room, namespace, and acknowledgment bookkeeping layered on top of the same underlying WebSocket.

Is Server-Sent Events lighter on server resources than WebSocket?

Not in our test. SSE connections cost 23.4-29.85 KB of server memory each, depending on the client implementation holding the stream open, which was closer to Socket.IO's cost than to raw WebSocket's. SSE's efficiency advantage is in its wire protocol, not necessarily in server memory per open connection.

When should I use SSE instead of WebSocket?

Use SSE when the data only needs to flow from server to client, such as AI token streaming, live notifications, or price updates the client never responds to on the same connection. SSE runs over ordinary HTTP, which means it is less likely to be blocked by strict proxies or firewalls than a WebSocket upgrade, and the browser's EventSource API handles reconnection for you.

Comments

Loading…

Sign in to join the conversation.

Related posts