WebSocket & Real-Time Patterns
Build real-time features with WebSockets and SSE: connection management, presence, rooms, horizontal scaling with Redis, and reconnection logic.
Real-time features (live collaboration, notifications, chat, dashboards) require persistent connections. WebSockets enable bidirectional communication; Server-Sent Events (SSE) provide simpler one-way push. Horizontal scaling requires a message broker (Redis) to route messages across server instances.
Use SSE for one-directional server→client push (notifications, live feeds) - simpler than WebSockets. Use WebSockets (Socket.io) for bidirectional communication (chat, collaboration, games). Always design for horizontal scaling from day one.
Workflow
- Choose WebSocket vs SSEUsing WebSockets when SSE suffices adds unnecessary complexity.
- Implement SSE for server-pushSSE uses standard HTTP - no special infrastructure or libraries needed for simple push.
- Build a WebSocket server with Socket.ioSocket.io adds rooms, namespaces, auto-reconnect, and fallback to polling over raw WebSockets.
- Scale horizontally with Redis adapterWithout a shared broker, WebSocket messages sent to server A don't reach clients connected to server B.
- Implement presence and connection stateUsers expect to see who's online; connection loss must be handled gracefully on both ends.
Output Format
SSE endpoint for server push, Socket.io server with rooms, Redis adapter for horizontal scale, presence tracking, reconnection handling on client.
Examples
SSE: `app.get("/events", (req, res) => { res.writeHead(200, { "Content-Type": "text/event-stream", "Cache-Control": "no-cache" }); const interval = setInterval(() => res.write(`data: ${JSON.stringify({ time: Date.now() })}
`), 1000); req.on("close", () => clearInterval(interval)) })`Socket.io room: `socket.on("send-message", async ({ roomId, text }) => { const msg = await db.messages.create({ roomId, userId: socket.data.userId, text }); io.to(roomId).emit("new-message", msg) })`Presence: `io.on("connection", async (socket) => { await redis.sadd(`presence:${socket.data.roomId}`, socket.data.userId); socket.on("disconnect", async () => { await redis.srem(`presence:${socket.data.roomId}`, socket.data.userId) }) })`Edge Cases & Gotchas
- Proxy buffering: Nginx buffers SSE responses by default; add `proxy_buffering off` in the location block for SSE endpoints
- Socket.io CORS: configure origins explicitly: `new Server(httpServer, { cors: { origin: "https://yourapp.com", credentials: true } })` - wildcard CORS breaks cookie auth
- Message ordering: WebSocket messages are ordered per connection but not across reconnections; include sequence numbers for collaborative editing to detect and reconcile gaps
When to Break These Rules
- For simple polling scenarios where data changes infrequently (every 30+ seconds), regular HTTP polling with a short interval is simpler than persistent connections and scales better.
Reference Files
Socket.io docs: https://socket.io/docs/MDN SSE: https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_eventsSocket.io Redis adapter: https://socket.io/docs/v4/redis-adapter/