Single-node · In-memory · gRPC

Heads up: I ended up switching to NATS + PostgreSQL. If you need consumer groups, bare NATS won't help — you need JetStream, which requires persistence. tinybroker fills the gap when you want in-memory consumer groups with no persistence overhead. The code works and is free to use; just know why you're reaching for it.

WebSocket & realtime coordination

WebSocket servers maintain persistent connections with clients. When you scale to multiple backend nodes, a message intended for a specific user may arrive at a node that does not hold that user’s WebSocket connection. tinybroker lets any node publish an event and every node that holds a relevant connection forward it — no sticky sessions required.

Architecture

Client A ── WS ── Node 1 ──┐
Client B ── WS ── Node 1 ──┤                    ┌── Node 1 (forwards to A, B)
Client C ── WS ── Node 2 ──┤── Publish ─► TB ──┤
Client D ── WS ── Node 2 ──┤                    └── Node 2 (forwards to C, D)
Client E ── WS ── Node 3 ──┘                        Node 3 (no match, drops)

TB = tinybroker. Each node subscribes to the full event stream. Each node forwards only the events relevant to its connected clients.

Per-user topics

Use a per-user topic so nodes can filter efficiently at the broker level rather than fan-out everything and filter locally:

topic: "user.{user_id}.notify"

Node 1 subscribes to user.abc.notify when user abc connects, removes the pattern when they disconnect. Only Node 1 receives messages for user abc.

// When client connects
stream.Send(&pb.SubscribeCommand{
    Command: &pb.SubscribeCommand_AddPatterns{
        AddPatterns: &pb.AddTopicPatterns{
            TopicPatterns: []string{"user." + userID + ".notify"},
        },
    },
})

// When client disconnects — remove the pattern to stop delivery
stream.Send(&pb.SubscribeCommand{
    Command: &pb.SubscribeCommand_RemovePatterns{
        RemovePatterns: &pb.RemoveTopicPatterns{
            TopicPatterns: []string{"user." + userID + ".notify"},
        },
    },
})

Room / channel topics

For chat rooms or collaborative spaces, use room-scoped topics:

"room.{room_id}.message"
"room.{room_id}.presence"

Every node that has at least one connection in a room subscribes to room.{room_id}.*. When a user sends a message, any node publishes it; all nodes with room members receive and forward it.

Broadcast to all connected clients

Use a single wildcard pattern per node:

OpenSubscription { topic_patterns: ["notify.*"] }

Publish notify.maintenance and every connected client receives the alert via their node’s WebSocket.

Presence and typing indicators

For high-frequency events like typing indicators, consider filtering at the subscriber:

topic: "room.{room_id}.typing"

Nodes subscribe at the room level. The handler checks whether any local client cares about that room before forwarding over WebSocket. This avoids per-user pattern churn for ephemeral events.

Scaling notes

  • Each node holds one long-lived subscription stream. Patterns on that stream are modified dynamically as users connect and disconnect — no reconnect needed.
  • Pattern additions and removals are acknowledged by the server with a patterns_updated frame so you know when routing has taken effect.
  • For extremely high client counts, consider batching pattern changes rather than adding/removing one pattern per client connection.