In this article
Modern web applications demand instant, bi-directional communication for collaborative document editing, real-time cryptocurrency trading tickers, multiplayer gaming, and live AI agent streaming. While traditional HTTP polling and Server-Sent Events (SSE) offer partial solutions, full-duplex WebSockets remain the gold standard for low-latency interactive experiences. However, scaling WebSocket infrastructure to handle hundreds of thousands of concurrent connections introduces severe concurrency, memory, and thread-management challenges.
In 2026, Go (Golang) paired with optimized WebSocket libraries remains the premier choice for production WebSocket gateways. Thanks to Go’s lightweight goroutines (consuming only ~2KB of stack memory compared to 1MB per OS thread in Java or C++), a single modest Linux server can easily sustain over 250,000 concurrent active WebSocket connections with sub-millisecond broadcast latency. This comprehensive engineering tutorial covers connection lifecycle management, Redis Pub/Sub horizontal clustering, and memory tuning.
Why Go Outperforms Node.js and Python for WebSockets
Evaluating server runtimes under high concurrent socket loads:
| Runtime & Framework | Max Concurrent Sockets (16GB RAM) | P99 Broadcast Latency | Idle Memory per 10k Sockets |
|---|---|---|---|
| Node.js (Socket.io) | ~35,000 connections (event-loop bound) | 48ms | 420 MB |
| Python (FastAPI + WebSockets) | ~20,000 connections | 85ms | 680 MB |
| Go (Gorilla / nhooyr.io) | 250,000+ connections | 1.8ms | 45 MB (89% memory savings) |
Step-by-Step Production Architecture: The Hub Pattern
1. Connection Hub and Client Structures (`hub.go`)
Implement the thread-safe Hub pattern to coordinate client registrations, unregistrations, and message broadcasts:
// hub.go
package main
import (
"sync"
)
type Hub struct {
clients map[*Client]bool
broadcast chan []byte
register chan *Client
unregister chan *Client
mu sync.RWMutex
}
func newHub() *Hub {
return &Hub{
broadcast: make(chan []byte, 256),
register: make(chan *Client),
unregister: make(chan *Client),
clients: make(map[*Client]bool),
}
}
func (h *Hub) run() {
for {
select {
case client := <-h.register:
h.mu.Lock()
h.clients[client] = true
h.mu.Unlock()
case client := <-h.unregister:
h.mu.Lock()
if _, ok := h.clients[client]; ok {
delete(h.clients, client)
close(client.send)
}
h.mu.Unlock()
case message := <-h.broadcast:
h.mu.RLock()
for client := range h.clients {
select {
case client.send <- message:
default:
close(client.send)
delete(h.clients, client)
}
}
h.mu.RUnlock()
}
}
}
2. Managing Client Read and Write Pumps (`client.go`)
Decouple socket reading from writing to prevent slow clients from blocking the hub:
// client.go
package main
import (
"log"
"time"
"github.com/gorilla/websocket"
)
const (
writeWait = 10 * time.Second
pongWait = 60 * time.Second
pingPeriod = (pongWait * 9) / 10
maxMessageSize = 512
)
type Client struct {
hub *Hub
conn *websocket.Conn
send chan []byte
}
func (c *Client) readPump() {
defer func() {
c.hub.unregister <- c
c.conn.Close()
}()
c.conn.SetReadLimit(maxMessageSize)
c.conn.SetReadDeadline(time.Now().Add(pongWait))
c.conn.SetPongHandler(func(string) error {
c.conn.SetReadDeadline(time.Now().Add(pongWait))
return nil
})
for {
_, message, err := c.conn.ReadMessage()
if err != nil {
break
}
c.hub.broadcast <- message
}
}
func (c *Client) writePump() {
ticker := time.NewTicker(pingPeriod)
defer func() {
ticker.Stop()
c.conn.Close()
}()
for {
select {
case message, ok := <-c.send:
c.conn.SetWriteDeadline(time.Now().Add(writeWait))
if !ok {
c.conn.WriteMessage(websocket.CloseMessage, []byte{})
return
}
c.conn.WriteMessage(websocket.TextMessage, message)
case <-ticker.C:
c.conn.SetWriteDeadline(time.Now().Add(writeWait))
if err := c.conn.WriteMessage(websocket.PingMessage, nil); err != nil {
return
}
}
}
}
Scaling Across Multi-Server Clusters with Redis Pub/Sub
A single server is bounded by file descriptors; scaling horizontally across multiple gateway nodes requires linking hubs via a distributed Redis Pub/Sub backplane:
// When message arrives on local node, publish to Redis cluster
rdb.Publish(ctx, "global_broadcast_channel", message)
// Background goroutine subscribes to Redis and forwards to local Hub
go func() {
pubsub := rdb.Subscribe(ctx, "global_broadcast_channel")
ch := pubsub.Channel()
for msg := range ch {
hub.broadcast <- []byte(msg.Payload)
}
}()
Linux OS Kernel Tuning for 250,000+ Sockets
By default, Linux limits open file descriptors per process to 1,024. Tune /etc/security/limits.conf and sysctl:
# /etc/security/limits.conf
* soft nofile 1048576
* hard nofile 1048576
# /etc/sysctl.conf
fs.file-max = 2097152
net.ipv4.tcp_max_syn_backlog = 8192
net.core.somaxconn = 65535
Conclusion
Building real-time systems in Go delivers unmatched concurrency, reliability, and cost-efficiency. By structuring connections using the Hub pattern, implementing decoupled read/write pumps, and scaling across nodes via Redis Pub/Sub, engineering teams build real-time communication backbones capable of serving millions of concurrent global users.


