A practical architecture guide for securing and scaling real-time WebSocket applications built with Node.js, using a matchmaking system as the example. Covers establishing server-owned sessions, validating and dispatching messages safely, authenticating before expensive work, rate-limiting individual actions rather than just connections, enforcing state machine transitions, making cleanup idempotent, detecting dead connections via heartbeats, applying backpressure under load, managing outbound buffer overflow, separating commands from events, minimizing broadcasted data, handling reconnection deliberately, moving counters to shared infrastructure like Redis when scaling horizontally, and instrumenting rejected operations alongside successes. Emphasizes treating every WebSocket message as untrusted input and designing for eventual disconnection.
Table of contents
Begin with a Strict Connection BoundaryGive Every Connection a Server-Owned SessionValidate Messages Before Dispatching ThemAuthenticate Before Expensive Work BeginsRate-Limit Actions, Not Just ConnectionsProtect State TransitionsMake Repeated Events Idempotent Where PossibleUse Heartbeats to Find Dead ConnectionsApply Backpressure Before the Server Is OverloadedControl Outbound Message Volume TooSeparate Commands From EventsAvoid Broadcasting More Information Than NecessaryTreat Reporting as a Server WorkflowUse Cooldowns for Expensive ActionsHandle Reconnection as a New QuestionMove Counters Out of Process When You ScaleInstrument Rejections, Not Just SuccessesLog Identifiers, Not Conversation ContentsTest Hostile Sequences, Not Just Valid OnesKeep the Gateway SmallDesign for the Connection You Will Eventually LoseConclusionQuestions this post answers
How do I rate-limit individual WebSocket actions instead of just HTTP connections in Node.js?
Attach a token bucket to each session and consume a token per action type rather than relying on connection-level HTTP rate limits. A bucket with a capacity and refill rate (for example capacity 4, refill 0.5 tokens per second) tracks tokens per session; each command like find_match checks and consumes a token before proceeding, rejecting the action if none remain. daily.dev surfaces backend architecture patterns for developers hardening persistent connection systems like WebSockets.
What WebSocket close code should a server use when it is at capacity?
Close code 1013 signals that the service is temporarily unavailable and the client should retry later. A server can check active session count against a maximum (such as 30,000) on each new connection attempt and immediately close incoming sockets with code 1013 and a reason like 'Server busy' instead of accepting connections it cannot support. Developers designing resilient real-time backends can track these patterns on daily.dev.
How do I detect dead WebSocket connections that never sent a close event?
Use ping/pong heartbeat frames with the ws package: mark each socket alive on connect and on every pong received, then run an interval (e.g. every 30 seconds) that terminates any socket still marked not-alive and otherwise flips it to not-alive and sends a new ping. This catches half-open connections from mobile network changes, laptop sleep, or crashes that never trigger a clean close event. daily.dev helps developers stay on top of real-time infrastructure patterns like connection health checks.