[HN Gopher] The Hidden Complexity of Scaling WebSockets
       ___________________________________________________________________
        
       The Hidden Complexity of Scaling WebSockets
        
       Author : atul-jalan
       Score  : 34 points
       Date   : 2025-01-24 19:48 UTC (3 hours ago)
        
 (HTM) web link (composehq.com)
 (TXT) w3m dump (composehq.com)
        
       | peteforde wrote:
       | This is all true, but it also serves to remind us that Rails
       | gives developers so much out of the box, even if you're not aware
       | of it.
       | 
       | ActionCable is Rails' WebSockets wrapper library, and it
       | addresses basically every pain point in the post. However, it
       | does so in a way that all Rails developers are using the same
       | battle-tested solution. There's no need for every project to hack
       | together its own proprietary approach.
       | 
       | Thundering herds, heartbeat monitoring are both covered.
       | 
       | If you need a messaging schema, I strongly recommend that you
       | check out CableReady. It's a powerful library for triggering
       | outcomes on the client. It ships with a large set of operations,
       | but adding custom operations is trivial.
       | 
       | https://cableready.stimulusreflex.com/hello-world/
       | 
       | While both ActionCable and CableReady are Rails libraries, other
       | frameworks would score huge wins if they adopted their client
       | libraries.
        
         | atul-jalan wrote:
         | Node has similar libraries like Socket.IO too, but it over-
         | abstracts it a bit in my opinion.
        
           | hombre_fatal wrote:
           | I've done my share of building websocket servers from
           | scratch, but when you don't use libraries like ActiveCable or
           | socket.io, you have to build your own MessageID
           | reconciliation so that you can have request/response cycles.
           | Which is generally what you want (or eventually want) in a
           | websocket-heavy application.
           | send(payload).then(reply => ...)
        
             | atul-jalan wrote:
             | Yep, for our application, we have an `executionId` that is
             | sent in essentially every single WebSocket message.
             | 
             | But client and server use it to maintain a record of
             | events.
        
       | exabrial wrote:
       | I recall another complication with websockets: IIRC it's with
       | proxy load balancers, like binding a connection to a single
       | connection server, even if the backend connection is using
       | HTTP/2. I probably have the details wrong. I'm sure someone will
       | correct my statement.
        
         | hpx7 wrote:
         | Horizontal scaling is certainly a challenge. With traditional
         | load balancers, you don't control which instance your clients
         | get routed to, so you end up needing to use message brokers or
         | stateful routing to ensure message broadcasts work correctly
         | with multiple websocket server instances.
        
         | atul-jalan wrote:
         | I think there is a way to do it, but it likely involves custom
         | headers on the initial connection that the load balancer can
         | read to route to the correct origin server.
         | 
         | I imagine the way it might go is that the client would first
         | send an HTTP request to an endpoint that returns routing
         | instructions, and then use that in the custom headers it sends
         | when initiating the WebSocket connection.
         | 
         | Haven't tried this myself though.
        
         | arccy wrote:
         | I think it's more that WebSockets are held open for a long
         | time, so if you're not careful, you can get "hot" backends with
         | a lot of connections that you can't shift to a different
         | instance. It can also be harder to rotate backends since you
         | know you are disrupting a large number of active clients.
        
       | 10000truths wrote:
       | The key to managing this complexity is to avoid mixing transport-
       | level state with application-level state. The same approach for
       | scaling HTTP requests also works for scaling WebSocket
       | connections:
       | 
       | * Read, write and track all application-level state in a
       | persistent data store.
       | 
       | * Identify sessions with a session token so that application-
       | level sessions can span multiple WebSocket connections.
       | 
       | It's a lot easier to do this if your application-level protocol
       | consists of a single discrete request and response (a la RPC).
       | But you can also handle unidirectional/bidirectional streaming,
       | as long as the stream states are tracked in your data store and
       | on the client side.
        
       ___________________________________________________________________
       (page generated 2025-01-24 23:00 UTC)