Scaling Go WebSockets Across Servers with Redis Pub/Sub
A single WebSocket server works until you add a second instance. The Go hub pattern for connections and rooms, and the Redis Pub/Sub bridge that makes horizontal scaling work.
WebSocket vs SSE vs Polling
WebSocket is bidirectional and persistent, so it suits chat and collaborative editing. SSE is server-to-client only, which suits notifications. For the community forum, WebSocket was the clear choice.
WebSocket Hub Pattern in Go
A central hub manages all connections, rooms, and message broadcasting. Each connection runs in its own goroutine with channels for communication.
go
type Hub struct { rooms map[string]map[*Client]bool // roomID -> clients broadcast chan *Message register chan *Client unregister chan *Client mu sync.RWMutex}func (h *Hub) Run() { for { select { case client := <-h.register: h.mu.Lock() if _, ok := h.rooms[client.RoomID]; !ok { h.rooms[client.RoomID] = make(map[*Client]bool) } h.rooms[client.RoomID][client] = true h.mu.Unlock() case client := <-h.unregister: h.mu.Lock() if clients, ok := h.rooms[client.RoomID]; ok { delete(clients, client) close(client.Send) if len(clients) == 0 { delete(h.rooms, client.RoomID) } } h.mu.Unlock() case msg := <-h.broadcast: h.mu.RLock() if clients, ok := h.rooms[msg.RoomID]; ok { for client := range clients { select { case client.Send <- msg.Data: default: close(client.Send) delete(clients, client) } } } h.mu.RUnlock() } }}
Scaling with Redis Pub/Sub
Single-server WebSocket breaks at scale. Redis Pub/Sub bridges multiple instances: publish on one server, all servers receive and broadcast to their connected clients.
go
// Subscribe to Redis and broadcast to local WebSocket clientsfunc (h *Hub) SubscribeToRedis(rdb *redis.Client) { pubsub := rdb.Subscribe(ctx, "chat:*") ch := pubsub.Channel() for msg := range ch { // msg.Channel = "chat:room123" roomID := strings.TrimPrefix(msg.Channel, "chat:") h.broadcast <- &Message{ RoomID: roomID, Data: []byte(msg.Payload), } }}// Publish message: goes to all server instancesfunc (h *Hub) PublishMessage(rdb *redis.Client, roomID string, data []byte) { rdb.Publish(ctx, "chat:"+roomID, data)}
Written by Hiren Limbasiya, tech lead and full-stack engineer. Questions or corrections are welcome.