Multiple WebSocket servers mean users on different servers can't receive each other's messages. Redis Pub/Sub solves this. Claude Code generates the correct architecture automatically from your CLAUDE.md.
CLAUDE.md for WebSocket Scaling
# WebSocket Rules
- Use @socket.io/redis-adapter; messages via Redis Pub/Sub
- Sessions stored in Redis (no sticky sessions required)
- Monitor connection count per user
- Max 5 connections per user
- 30-minute timeout for inactive connections
- JWT auth on handshake; invalid token = immediate disconnect
createSocketServer()
import { Server } from 'socket.io';
import { createAdapter } from '@socket.io/redis-adapter';
import { createClient } from 'redis';
export async function createSocketServer(httpServer: any) {
const io = new Server(httpServer, {
cors: { origin: process.env.ALLOWED_ORIGINS?.split(','), credentials: true },
transports: ['websocket', 'polling'],
});
const pubClient = createClient({ url: process.env.REDIS_URL });
const subClient = pubClient.duplicate();
await Promise.all([pubClient.connect(), subClient.connect()]);
io.adapter(createAdapter(pubClient, subClient));
applyAuthMiddleware(io);
applyConnectionLimitMiddleware(io, pubClient);
registerConnectionHandler(io, pubClient);
return io;
}
JWT Auth Middleware
function applyAuthMiddleware(io: Server) {
io.use((socket, next) => {
const token = socket.handshake.auth.token;
if (!token) return next(new Error('Authentication required'));
try {
const payload = jwt.verify(token, process.env.JWT_SECRET!) as JwtPayload;
socket.data.userId = payload.userId;
socket.data.tenantId = payload.tenantId;
next();
} catch {
next(new Error('Invalid token'));
}
});
}
Connection Limit Middleware (Redis Set)
function applyConnectionLimitMiddleware(io: Server, redis: RedisClientType) {
io.use(async (socket, next) => {
const connectionKey = `connections:${socket.data.userId}`;
const count = await redis.sCard(connectionKey);
if (count >= 5) {
return next(new Error('Connection limit exceeded (max 5)'));
}
await redis.sAdd(connectionKey, socket.id);
await redis.expire(connectionKey, 1800); // 30min TTL
socket.on('disconnect', async () => {
await redis.sRem(connectionKey, socket.id);
});
next();
});
}
Connection Handler
function registerConnectionHandler(io: Server, redis: RedisClientType) {
io.on('connection', (socket) => {
const { userId, tenantId } = socket.data;
// Auto-join tenant and user rooms
socket.join(`tenant:${tenantId}`);
socket.join(`user:${userId}`);
// Room join with access check
socket.on('join:room', async (roomId: string) => {
const hasAccess = await checkRoomAccess(userId, roomId);
if (!hasAccess) {
socket.emit('error', { message: 'Access denied' });
return;
}
socket.join(roomId);
});
// Message event: save to DB + broadcast via adapter (all servers)
socket.on('message', async (data: MessagePayload) => {
const message = await saveMessageToDB(data, userId);
io.to(data.roomId).emit('message:new', message);
});
// Inactive timeout: 30 minutes
const inactiveTimer = setTimeout(() => {
socket.disconnect(true);
}, 30 * 60 * 1000);
socket.on('disconnect', () => clearTimeout(inactiveTimer));
});
}
Broadcasting Helpers
// Broadcast to all users in a tenant (all servers via adapter)
export function broadcastToTenant(io: Server, tenantId: string, event: string, data: any) {
io.to(`tenant:${tenantId}`).emit(event, data);
}
// Notify a specific user across all their connections
export function notifyUser(io: Server, userId: string, event: string, data: any) {
io.to(`user:${userId}`).emit(event, data);
}
Why This Works
-
Redis Adapter:
io.to(roomId).emit()reaches users on any server — the adapter routes messages through Redis Pub/Sub automatically - No sticky sessions: Any server can handle any client; session state lives in Redis
- Connection tracking: Redis Set per user; enforces the 5-connection limit across all server instances
- JWT on handshake: Auth runs once at connect time, before any events are processed
Summary
CLAUDE.md defines the constraints → Claude Code generates createSocketServer() with the Redis adapter wired up, JWT middleware, connection limit enforcement via Redis Set, and room-based broadcasting that works across every server instance.
Want Claude Code to generate safe, production-ready code from your specs?
Code Review Pack (¥980) — prompt collection for architecture review, security check, and refactoring → prompt-works.jp
Top comments (0)