Socket.IO at scale: which Redis adapter survives a Redis restart
By Nihar Ranjan Das · Mon Oct 19 2026 · 10 min read · 0 views
View as a Web StorySoftware#node.js#Redis#socket.io#websocket#scaling

Socket.IO's Streams adapter lost zero messages across four Redis restarts in our test. The classic Redis adapter and the newer sharded adapter each lost over half their messages in three of those four runs. We ran two Socket.IO servers. We broadcast 400 messages while killing and restarting Redis mid-stream. Then we counted what arrived on the other side.
The difference is not an edge case. It is what happens the first time your production Redis fails over, restarts for a patch, or drops a connection during a routine deploy.
What is the Socket.IO Redis adapter, and why do you need one?
The Socket.IO Redis adapter is a plugin. It lets multiple Socket.IO server processes broadcast to each other's connected clients over Redis pub/sub. With it, io.emit() on one server reaches a socket connected to another server. Without it, a Socket.IO deployment with more than one instance only delivers messages to clients on the same process that sent them. A chat message from a user on server A never reaches a user on server B.
Pub/sub is Redis's publish-subscribe messaging pattern, described in Redis's own pub/sub documentation. A publisher sends a message to a channel, and only clients subscribed to that channel at that exact moment receive it. Redis does not store or replay pub/sub messages — a subscriber that is reconnecting when a message is published never sees it, which is the entire reason this test exists.
Sticky sessions are a load balancer setting that always routes a given client's requests to the same backend server, usually keyed on IP address or a cookie. Without them, a client's Socket.IO handshake requests can land on different servers that don't share session state, and the connection fails before it starts.
Socket.IO ships three ways to do this, and they are not interchangeable:
- The classic Redis adapter (
@socket.io/redis-adapter,createAdapter()). This is the original implementation, built on standard Redis pub/sub. - The sharded adapter (same package,
createShardedAdapter()). It uses Redis 7.0's sharded pub/sub feature. The official docs now recommend it for new projects. - The Redis Streams adapter (
@socket.io/redis-streams-adapter). This is a newer, separate package built on Redis Streams instead of pub/sub. It keeps a log of messages rather than firing and forgetting.
All three need sticky sessions on your load balancer. Socket.IO's handshake starts over HTTP long-polling before it can upgrade to WebSocket. If those early requests land on different servers, the client gets an HTTP 400 and never connects. This is true for every adapter. The adapter only fixes cross-server broadcast, not the handshake itself.
The test: kill Redis mid-broadcast, count what arrives
We ran this on one machine with Node.js 22.23.3, socket.io 4.8.4, @socket.io/redis-adapter 8.3.0, @socket.io/redis-streams-adapter 0.3.1, and the redis npm client 6.3.0, talking to a local Redis 8.4 instance. The setup:
- Two Socket.IO server instances, each wired to the same Redis adapter under test, listening on separate ports.
- One Socket.IO client connects to server B and counts every
broadcastevent it receives. - Server A emits 400 numbered broadcast events, one every 15 milliseconds, through the adapter.
- One second into the emit loop, Redis is shut down with
redis-cli shutdown nosave. Three seconds later, it is restarted. - After all 400 sends finish, we compare messages sent to messages received.
That is a roughly 3-second Redis outage landing in the middle of a 6-second broadcast run. It is short enough to resemble a Redis failover or a container restart, not a prolonged incident. This methodology isolates one variable — the adapter — while holding the network, the message rate, and the outage length constant across all three.
We ran four full trials per adapter, restarting Redis fresh before each one, rather than relying on a single run. A single run can land on either side of a reconnect race by chance; four runs show the pattern instead of one lucky or unlucky sample.
Advertisement
The results
| Adapter | Runs | Messages lost per bad run | Runs with zero loss |
|---|---|---|---|
Classic (createAdapter) |
4 | 229-239 of 400, 57-60% (2026 test) | 1 of 4 |
Sharded (createShardedAdapter) |
4 | 224-228 of 400, 56-57% (2026 test) | 1 of 4 |
Streams (createAdapter from the Streams package) |
4 | 0 | 4 of 4 |

The classic and sharded adapters behaved identically to each other, and inconsistently from run to run. One run out of four completed with zero loss. The other three each lost well over half the broadcast. The variance comes down to timing. The underlying redis npm client reconnects on its own schedule. Whether that reconnect lands before or after the next publish decides whether that message survives. Neither adapter buffers a message once it fails to deliver. Pub/sub has no memory of what it dropped — a published message with no subscriber listening at that instant is simply gone.

The Streams adapter never lost a message, in any of the four runs. The reason is in its name. Socket.IO writes every broadcast onto a Redis stream, which is a persistent, ordered log, not a fire-and-forget channel. When a server reconnects, it resumes reading the stream from where it left off, instead of waiting for the next message. The adapter's maxLen option caps that stream at 10,000 entries by default. That cap exists specifically so the stream does not grow forever during a long outage.
Why the reconnect race decides classic and sharded's loss rate
The redis npm client reconnects on its own schedule after a connection drops, controlled by a reconnectStrategy function. The client's own documentation describes the default as an increasing backoff — it does not retry instantly, and it does not guarantee a reconnect completes before your application code tries to publish again.
That default behavior explains the variance we measured. In the one-of-four runs where classic and sharded lost nothing, the publishing client happened to reconnect before Redis came back online long enough for the next scheduled broadcast to need it. In the other three runs, broadcasts kept firing on the 15-millisecond interval while the client was still mid-reconnect, and every one of those publishes went nowhere — pub/sub has no queue to catch them. Socket.IO's own adapter code does not retry a failed publish; it relies entirely on the Redis client underneath it, which is standard pub/sub behavior, not a Socket.IO-specific bug.
What this means for your deployment
If your Socket.IO servers can lose their Redis connection even briefly, the sharded adapter does not protect you more than the classic one does. And in production, they can: Redis restarts for maintenance, fails over to a replica, or just drops a TCP connection under load. The official docs recommend sharded over classic for a different reason. It uses Redis 7's sharded pub/sub to spread load across a Redis Cluster, which helps throughput at scale. It was never built to survive an outage, and our test shows it doesn't.
The Streams adapter trades that reliability for a few real constraints:
- It needs Redis 6.2 or later. Redis Streams are an append-only log data type, added to Redis in version 5.0, and the adapter's blocking reads need the newer command set from 6.2.
- Every missed message replays from the stream on reconnect. That can mean a burst of back-dated events hitting your client right after a Redis blip. Your client code needs to handle messages arriving out of real-time order.
- The default 10,000-entry
maxLenmeans an outage longer than it takes to produce 10,000 broadcasts will still lose the oldest of them. You can raisemaxLenif your traffic is high enough that this matters, but that costs Redis memory.
For example, a chat app, a live dashboard, or any feature where a dropped notification is a real user-facing bug is where the Streams adapter earns its cost — it is the one that actually delivered on our test bench. For a high-throughput fan-out where an occasional dropped update doesn't matter, and raw pub/sub throughput does, the sharded adapter's design intent of spreading load is still the right trade. Consider the two together: pick Streams by default, and switch to sharded only once you can show Redis pub/sub throughput, not reliability, is your bottleneck.
What this costs in Redis memory and ops effort
None of these adapters are free to run. The classic and sharded adapters need Redis only as a message bus — memory use stays close to zero beyond the connection overhead, since pub/sub never stores anything. The Streams adapter trades that for its replay guarantee: every broadcast sits in the stream until it ages past maxLen entries, which is real Redis memory for the lifetime of each message.
For a server broadcasting 100 messages a second, a 10,000-entry stream holds about 100 seconds of history — fine for a Redis restart, not enough for a longer network partition. Raising maxLen to cover a 10-minute outage at that rate means sizing for 60,000 entries per stream instead, which Redis's own sizing guidance for streams frames as a memory-versus-durability tradeoff, not a free upgrade.
Operationally, the Streams adapter is also the newer, less-used package of the three. @socket.io/redis-streams-adapter sits on its own release cadence, separate from the core @socket.io/redis-adapter package, so pin its version and test upgrades the same way you would test a Socket.IO major version bump. The @socket.io/redis-adapter package's own compatibility table lists which Socket.IO server versions each adapter release supports — check it before bumping either package independently of the other.
Checking which adapter you are actually running
A production incident is the wrong time to discover which adapter a service uses. Grep your package.json for @socket.io/redis-adapter or @socket.io/redis-streams-adapter — a project can only sensibly run one, since they attach to the same io.adapter() call. If you see createAdapter imported from @socket.io/redis-adapter, you're on classic. If you see createShardedAdapter from the same package, you're on sharded. If the import comes from @socket.io/redis-streams-adapter instead, you're already on Streams and these numbers don't apply to you.
Setting it up
Sticky sessions come first, regardless of which adapter you pick. With nginx:
upstream socket_nodes {
ip_hash;
server 127.0.0.1:4001;
server 127.0.0.1:4002;
}
Then the Streams adapter itself:
import { createClient } from "redis";
import { createAdapter } from "@socket.io/redis-streams-adapter";
import { Server } from "socket.io";
const redisClient = createClient({ url: "redis://localhost:6379" });
await redisClient.connect();
const io = new Server(httpServer, {
adapter: createAdapter(redisClient, { maxLen: 10_000 }),
});
Unlike the classic and sharded adapters, the Streams adapter needs only one Redis client. It does not need a separate publisher and subscriber pair, which is one less connection to manage per server instance.
For comparison, the classic adapter needs two clients and an extra import:
import { createClient } from "redis";
import { createAdapter } from "@socket.io/redis-adapter";
import { Server } from "socket.io";
const pubClient = createClient({ url: "redis://localhost:6379" });
const subClient = pubClient.duplicate();
await Promise.all([pubClient.connect(), subClient.connect()]);
const io = new Server(httpServer, {
adapter: createAdapter(pubClient, subClient),
});
Swapping from classic to Streams later is a small change. You keep the same Server setup and only change the adapter import and its single client.
What we did not test
This test covers one failure mode: a short, clean Redis restart on one node. It does not cover a Redis Cluster failover, network partitions that split brain your servers, or Redis running out of memory mid-stream. Each of those is a different failure shape, and some of them would stress the Streams adapter's maxLen cap in ways a clean restart never does. Treat these numbers as evidence for "Redis went away and came back," not as a guarantee for every outage your infrastructure can produce.
A decision checklist
Use this before picking an adapter for a new deployment:
- Can your service tolerate silently dropped broadcasts during a Redis restart? If no, the Streams adapter is the only one of the three that delivered on this test, and the other two are not a safe default.
- Are you scaling Redis itself across a cluster for pub/sub throughput, separate from reliability? If yes, the sharded adapter's design intent still applies — just don't expect it to also protect you during an outage, since our test shows it doesn't.
- Is your load balancer configured for sticky sessions already? If not, fix that first. Every adapter here fails the same way without it, and no adapter choice compensates for missing sticky sessions.
- Can your client code handle a burst of replayed, out-of-order events after a brief outage? The Streams adapter's reliability comes with this tradeoff, and it needs handling in your event listeners, not just in your infrastructure.
- Is your Redis instance memory-constrained already? If yes, size the Streams adapter's
maxLencarefully — raising it to survive a longer outage costs Redis memory per stream, on top of whatever else runs on that instance.
Advertisement
FAQ
Does the Socket.IO Redis adapter need sticky sessions?
Yes, every Socket.IO Redis adapter - classic, sharded, and Streams - still requires sticky sessions on the load balancer. The adapter only handles cross-server broadcast after a client connects; the initial handshake uses HTTP long-polling, and if those requests reach different servers, Socket.IO returns an HTTP 400 before the connection is established.
Which Socket.IO adapter should I use for production?
Use the Streams adapter if message delivery during a Redis outage matters to your application, since it replays missed messages instead of dropping them. Use the sharded adapter if you're scaling a Redis Cluster for throughput and can tolerate occasional message loss during a Redis restart, since classic and sharded performed identically in our outage test.
Can the sharded Socket.IO adapter run on a Redis Cluster?
Not through the ioredis client. The official docs state it is not currently possible to use the sharded adapter with ioredis against a Redis Cluster; use the redis npm client instead if you need both.
How much data does the Redis Streams adapter keep?
By default, up to 10,000 entries per stream, set by the adapter's maxLen option. Older entries are trimmed once the stream exceeds that length, so a Redis outage longer than it takes to generate 10,000 broadcasts will still lose the oldest messages.
Comments
Loading…
Sign in to join the conversation.
Related posts

What Claude's new OpenTelemetry traces actually send out
Claude API OpenTelemetry traces are now in public beta, and the default setting sends no prompts at all. We ran the Anthropic Python SDK against a mock server and counted what leaves your app.
Mon Oct 19 2026 · 10 min read · 0 views

Porting AutoGen to Microsoft Agent Framework: what breaks
An AutoGen to Microsoft Agent Framework migration is mostly a rename, except for four defaults that change silently. We ported a two-agent writer and reviewer team on Python 3.15.0, from AutoGen 0.7.5
Mon Oct 19 2026 · 10 min read · 0 views

MCP went stateless: what breaks when versions mix
The MCP 2026-07-28 migration is less frightening than the headlines suggest. The Model Context Protocol removed sessions and its connection handshake, and the official MCP blog says the release
Mon Oct 19 2026 · 10 min read · 0 views