Consistent Hashing for WebSockets: Sticky Gateways During Scale Events

Consistent Hashing for WebSockets: Sticky Gateways During Scale Events
14 views
WebSocket gateways need sticky affinity: the process that holds the socket must receive the pushes. Naive modulo hashing reshuffles almost everyone when you add a node; consistent hashing moves only ~1/N. Seniors combine a hash ring with graceful handoff so scale-in drains sockets instead of hard-cutting them mid-conversation.
⚡ TL;DR: Put gateways on a hash ring keyed by connectionId or userId. On scale events, migrate only the affected arc; drain with GOAWAY-style close codes and client reconnect jitter. Pair with API Gateway WebSocket Fan-Out and Graceful Shutdown for Node.

Consistent Hashing for WebSockets: production guidance

typescript
// ws/hash-ring.ts
import { createHash } from "node:crypto";

export class HashRing {
 private ring: { hash: number; node: string }[] = [];
 constructor(nodes: string[], vnode = 64) {
 for (const n of nodes) {
 for (let i = 0; i < vnode; i++) {
 const hash = this.h(`${n}#${i}`);
 this.ring.push({ hash, node: n });
 }
 }
 this.ring.sort((a, b) => a.hash - b.hash);
 }
 private h(s: string) {
 return createHash("sha256").update(s).digest().readUInt32BE(0);
 }
 owner(key: string): string {
 const x = this.h(key);
 const i = this.ring.findIndex((e) => e.hash >= x);
 return (this.ring[i] ?? this.ring[0]).node; // ✅ wrap
 }
}
❌ nodes[crc32(userId) % nodes.length] — adding one gateway remaps most users.

Sticky registry and push path

typescript
// ws/registry.ts
export async function bindConnection(connId: string, gatewayId: string) {
 await redis.set(`ws:${connId}`, gatewayId, "EX", 3600);
}

export async function publishToUser(userId: string, payload: unknown) {
 const conns = await redis.smembers(`user:${userId}:conns`);
 await Promise.all(
 conns.map(async (connId) => {
 const gw = await redis.get(`ws:${connId}`);
 if (!gw) return;
 // ✅ Direct to owning gateway; no broadcast storm
 await bus.send(gw, { connId, payload });
 })
 );
}

Graceful migration on scale-in

typescript
// ws/drain.ts
export async function drainGateway(gw: string, ring: HashRing) {
 const local = listLocalConnections();
 for (const c of local) {
 if (ring.owner(c.userId) === gw) continue; // still ours
 // Soft close — client reconnects with jitter into new owner
 c.socket.close(4001, "migrate");
 await sleep(5 + Math.random() * 50);
 }
 await deregisterFromRing(gw);
}
Event Action
Scale-out Add node to ring; optionally proactive migrate arc
Scale-in Drain first; remove from ring after 0 local sockets
Crash Clients reconnect; registry TTL expires stale binds
Deploy Same as scale-in per task

Client reconnect jitter

typescript
// client reconnect (browser / RN)
function backoff(attempt: number) {
 const base = Math.min(1000 * 2 ** attempt, 15000);
 return base * (0.5 + Math.random()); // ✅ avoid reconnect storms
}
Coordinate with API Gateway WebSocket Fan-Out so reconnect storms do not amplify pushes.

Closing checklist

✅ Dos – ✅ Use consistent hashing with vnodes – ✅ Bind connection→gateway in a TTL registry – ✅ Drain before removing a node from the ring – ✅ Soft-close with reconnect jitter – ✅ Push only to owning gateway ❌ Don’ts – ❌ Don’t use modulo hashing for sticky WS – ❌ Don’t broadcast every message to all gateways – ❌ Don’t kill tasks without drain – ❌ Don’t forget registry TTL after crashes – ❌ Don’t reconnect all clients on the same millisecond

Related reading

Last updated on September 11, 2026 Related: WebSockets vs Server-Sent Events vs Long Polling: Real-Time Communication Explained; API Gateway WebSockets for Multi-Turn Coding Agents: Connection State Without Sticky Myths; Node.js Cluster Module: Scale Your API to All CPU Cores Without Kubernetes.

Watch: 100 Days of AI on YouTube

Short videos from CheatCoders, one AI engineering topic per day.

Open the playlist on YouTube · CheatCoders on YouTube

Deep-dive PDF

Get the expanded guide for this post — extra diagrams-style checklists, failure modes, and a production walkthrough. Free when you subscribe to CheatCoders.

Already subscribed? or open the subscribe page.


Discover more from CheatCoders

Subscribe to get the latest posts sent to your email.

Comments

No comments yet. Why don’t you start the discussion?

Leave a comment

No account needed. Name and email are optional.