How to Rate-Limit Real-Time Connections
HTTP rate limits do not cover WebSockets. Rate-limit real-time connections at connect, per message, and per subscription, or one client can flood your whole server.
Most teams rate-limit their REST API and assume they are covered. They are not. A WebSocket is a single long-lived connection that then carries an unbounded stream of messages, and your HTTP rate limiter, which counts requests, sees exactly one request: the connection upgrade. After that, a client can send ten thousand messages a second and your rate limiter never notices, because to it nothing new happened. Real-time apps need their own rate limits, applied in three places the HTTP layer cannot reach: at connect, per message, and per subscription. Skip them and a single client, malicious or just buggy, can flood a server that your API rate limiter thinks is perfectly protected.
Why HTTP rate limiting misses the whole problem
An HTTP rate limiter counts requests over time and blocks clients that exceed a threshold. It works because each unit of work is a request, so counting requests counts work. WebSockets break that assumption. The expensive part is not opening the connection; it is everything that flows through it afterward, and none of that is a new HTTP request. One upgrade request buys a client an open channel through which they can do arbitrary amounts of work that your request counter is blind to.
So the mental model has to change. On a real-time connection, the unit of work is the message and the subscription, not the connection. You have to count and limit those, at the socket layer, with the identity of the connection in hand. This is the same shift I described for authorization in authorizing every real-time subscription: the meaningful action moved from the HTTP request to the socket event, so your controls have to move there too.
Limit at connect: how many, how fast
The first limit is on connections themselves. A single user or IP opening connections in a tight loop can exhaust your connection capacity before any messages are even sent, and each connection carries setup cost, memory, and an authentication check. Cap how many concurrent connections one identity may hold, and cap how fast new connections can be opened, so a client cannot hammer your accept path.
This protects against both accidents and attacks. A buggy client stuck in a reconnect loop can open connections furiously, which is why this pairs with sane reconnection handling that backs off instead of retrying instantly. A connection flood from one source should hit a wall quickly rather than eating capacity that legitimate users need.
Limit per message: the real firehose
The most important limit is per message, because that is where the volume lives. Once connected, a client can send messages as fast as it likes, and without a limit, one client can saturate a server's message-processing capacity on its own. Cap the message rate per connection, and decide what happens on breach: drop the excess, or disconnect a client that keeps blowing past the limit.
The right cap depends on the app. A chat client sends a message when a human hits enter, so a low limit is plenty and anything above it is abuse or a bug. A collaborative app streaming cursor movements legitimately sends more, but even that should be throttled by the client and bounded by the server, which connects to the coalescing and throttling I covered in building a live dashboard backend. The point is that the server must enforce a ceiling regardless of what the client claims it needs, because you cannot trust the client to police itself.
Per-message limiting also protects you from the inbound version of the backpressure problem. Just as a slow client reading too slowly is a risk, a fast client writing too quickly is one, and I treated the outbound side in handling backpressure from slow clients. Rate limiting the inbound stream is the other half of keeping one connection from destabilizing the server.
Limit per subscription and per action
The third place is subscriptions and privileged actions. Subscribing has real cost, so limit how many channels a connection can subscribe to and how fast it can churn subscriptions, or a client can force expensive setup work by rapidly joining and leaving. Likewise, actions that trigger heavy work, a broadcast to a large room, a history query, deserve their own limits so one client cannot repeatedly trigger the most expensive operations you support.
Tie these limits to identity, not just connection, so a client cannot dodge them by opening many connections. The limit should follow the authenticated user across all their connections, or the whole scheme is trivially defeated.
Make it the platform's job
Real-time rate limiting, at connect, per message, per subscription, tied to identity, is a lot to build correctly, and it is exactly the kind of protection that is invisible until the day you need it and catastrophic when it is missing. I want it enforced by the infrastructure rather than reimplemented per app, which is one reason I run real-time workloads on AltoHost: the limits live at the platform layer, close to the connection, instead of being bolted onto application code that never sees the flood coming. For the wider set of things that break real-time systems under load, I collected them in real-time backend mistakes at scale.
The test is to point a script at your socket and send messages as fast as you can. If your server happily processes all of them and starts to strain, your HTTP rate limiter gave you a false sense of safety and your real-time layer is unprotected. If the flood hits a ceiling and the abusive connection gets throttled or dropped while everyone else stays healthy, you rate-limited the right layer. Count messages and subscriptions, not just connections, and one loud client stays one loud client's problem.