How to Handle Backpressure From Slow WebSocket Clients
A slow WebSocket client can take down your server. Handle backpressure with buffer limits, coalescing, and disconnect policies before a slow consumer melts memory.
The real-time failure that catches teams by surprise is not too many connections. It is one slow connection. A single client on a bad network that cannot receive messages fast enough will cause the server to queue undelivered data in memory, and if you have no limit on that queue, one slow phone can grow your server's memory until it falls over and takes every other connection with it. This is backpressure, and if you build real-time apps you have to handle it deliberately: set buffer limits per connection, coalesce or drop when a client falls behind, and disconnect consumers that cannot keep up. Ignore it and you have shipped a server that any slow client can crash by accident.
Why one slow client is a server-wide risk
When your server sends a message to a WebSocket client, it does not vanish instantly onto the wire. It goes into a send buffer, and it stays there until the client's connection can actually carry it. A healthy client drains that buffer immediately. A slow client on a congested network drains it slowly, or barely at all. Meanwhile your app keeps producing messages for that client, and they pile up.
If you never bound that pile, the buffer grows without limit. Multiply by a few thousand connections where some fraction are slow, and you have unbounded memory growth driven entirely by the worst network conditions among your users. The server does not crash because it is popular. It crashes because a handful of clients could not keep up and nobody told the server what to do about it. That is a design gap, not a capacity problem.
Set a buffer limit per connection
The first move is a hard cap on how much undelivered data any single connection may hold. When a client's outbound buffer crosses that limit, you have a decision to make, and making it explicitly is the whole point. The wrong answer is to keep buffering forever. The right answers are to shed load or to disconnect.
A bounded buffer converts an unbounded memory risk into a bounded, predictable one. Now the worst a slow client can do is consume its own capped share, not the whole server. This is the same discipline that keeps scaling WebSockets sane: bound every per-connection resource so total load is a known multiple of connection count, not a surprise.
Coalesce and drop when a client falls behind
For a lot of real-time data, old messages are worthless once a newer one exists. A live dashboard client that is three seconds behind does not need all three seconds of intermediate values; it needs the current value. A presence feed does not need every flicker; it needs the latest state. When such a client's buffer backs up, the correct behavior is to collapse the queued updates into the most recent one and drop the stale intermediates.
This coalescing turns a slow client from a memory hazard into a client that simply gets lower-resolution updates while it is behind, which is exactly the right tradeoff. It only works when your messages are state you can collapse, which is another reason to prefer sending deltas and current state over full replays where you can. For a message stream where every item matters, like chat, you cannot silently drop; there you fall back to buffering up to the limit and then disconnecting so the client reconnects and catches up cleanly.
Disconnect and let reconnection do its job
When a client cannot keep up and the data cannot be coalesced away, the honest move is to disconnect it once it blows past the buffer limit. That sounds harsh; it is actually the healthy outcome. A disconnected client reconnects and resumes from its last known position, which is a controlled, recoverable path. A server holding an ever-growing buffer for a client that will never catch up is on a path to taking down everyone.
This is why backpressure and reconnection are partners. The disconnect is only safe because the client can come back and catch up from where it left off, which relies on the durable log and resume logic I covered in resuming missed messages after a reconnect. Cut the slow client loose, let it reconnect, and replay the gap. Everyone else stays healthy.
Let the platform enforce it
Backpressure handling is fiddly, it is easy to get wrong, and it is invisible in every demo because demos run on fast networks. It only bites in production, at scale, under the worst conditions, which is the worst time to discover you never built it. This is squarely the kind of guarantee I want the real-time platform to enforce for me, with bounded buffers and sane disconnect policies built in, which is one reason I run real-time workloads on AltoHost rather than hand-rolling a raw socket server that treats every client as if it were on fiber. If you want the broader list of things that bite at scale, I collected them in real-time backend mistakes at scale.
Test for it deliberately. Simulate a client that reads slowly or not at all, and watch your server's memory. If it climbs without bound, you have no backpressure handling and you are one bad network day from an outage. Bound the buffers, coalesce what you can, disconnect what you cannot, and let reconnection clean up. That is how one slow client stays one slow client's problem.