Skip to main content
Version: 1.7

Hub

The central connection manager that handles all WebSocket clients, rooms, and message routing.

import "github.com/KARTIKrocks/wshub"
  • Manages all connected WebSocket clients
  • Broadcasts to all clients, specific clients, or rooms
  • O(1) client and user lookups via hash maps
  • Snapshot-based lock-free broadcasting
  • Optional parallel broadcasting for 1000+ clients
  • Multi-node support via pluggable adapters
  • Configurable backpressure with drop policies
  • Graceful shutdown with context support
  • Built-in health and readiness handlers for Kubernetes probes

Exact signatures live on pkg.go.dev.

Creating a Hub

Create a hub with functional options and start the run loop:

hub := wshub.NewHub(
wshub.WithConfig(config),
wshub.WithLogger(logger),
wshub.WithMetrics(metrics),
wshub.WithLimits(limits),
wshub.WithHooks(hooks),
wshub.WithMessageHandler(handler),
wshub.WithParallelBroadcast(100), // batch size for parallel broadcast
wshub.WithAdapter(adapter), // multi-node support
wshub.WithPresence(5*time.Second),// cluster-wide counts
wshub.WithDropPolicy(wshub.DropOldest), // backpressure control
)

// Start the hub run loop (required)
go hub.Run()

// Register as HTTP handler
http.HandleFunc("/ws", hub.HandleHTTP())

// UpgradeConnection with options (e.g., set user ID atomically)
client, err := hub.UpgradeConnection(w, r, wshub.WithUserID("user-123"))

Hub Options

OptionDescription
WithConfig(cfg)Set WebSocket configuration (buffer sizes, timeouts, compression)
WithLogger(l)Set a custom logger implementation
WithMetrics(m)Set a metrics collector
WithLimits(l)Set connection and rate limits
WithHooks(h)Set lifecycle hooks
WithMessageHandler(fn)Set the message handler function
WithParallelBroadcast(n)Enable parallel broadcasting with batch size n
WithParallelBroadcastWorkers(n)Set the number of persistent worker goroutines for parallel broadcasting (default: runtime.NumCPU()). No effect unless WithParallelBroadcast is also set
WithAdapter(adapter)Set multi-node adapter for cross-node message delivery
WithNodeID(id)Set a stable node identifier for debugging (default: random UUID)
WithPresence(interval)Enable periodic presence gossip for global counts
WithHookTimeout(d)Max wait for synchronous hooks like BeforeDisconnect (default: 5s)
WithDropPolicy(policy)Set backpressure behaviour when the send buffer is full (DropNewest or DropOldest)
WithoutHandlerLatency()Disable built-in latency recording (use with MetricsMiddleware to avoid double-counting)
WithDrainTimeout(d)Max idle time before closing a connection during drain (default: 30s). Set to 0 to disable the idle reaper

Broadcasting

MethodDescription
Broadcast(data)Send text bytes to all connected clients
BroadcastText(text)Send a text string to all clients
BroadcastBinary(data)Send binary data to all clients
BroadcastJSON(v)JSON-encode and send to all clients
BroadcastRawJSON(data)Broadcast pre-serialized JSON bytes to all clients (0 allocs, skips marshaling)
BroadcastWithContext(ctx, data)Broadcast with context support
BroadcastExcept(data, except...)Send text to all except specified clients
BroadcastBinaryExcept(data, except...)Send binary to all except specified clients
SendToClient(clientID, data)Send text to a specific client by ID
SendBinaryToClient(clientID, data)Send binary to a specific client by ID
SendToClientWithContext(ctx, clientID, data)Send to client with context (blocks until enqueued)
SendToUser(userID, data)Send text to all connections of a user
SendBinaryToUser(userID, data)Send binary to all connections of a user
SendToUserWithContext(ctx, userID, data)Send to user with context (blocks until enqueued)
// Broadcast to all connected clients
hub.Broadcast([]byte("hello everyone"))
hub.BroadcastText("hello everyone")
hub.BroadcastJSON(map[string]string{"type": "notification", "msg": "hello"})

// Send to specific client or user
hub.SendToClient(clientID, []byte("private message"))
hub.SendToUser(userID, []byte("sent to all devices"))

// Broadcast to all except certain clients
hub.BroadcastExcept([]byte("hello others"), excludedClient1, excludedClient2)

Client Lookup

MethodDescription
Clients()Get all connected clients
ClientCount()Get count of connected clients (atomic, no lock)
GetClient(id)O(1) client lookup by ID
GetClientByUserID(userID)Get first client for a user
GetClientsByUserID(userID)Get all connections for a user
NodeID()Get this hub's unique node identifier
// Look up clients
client, ok := hub.GetClient(clientID)
if ok {
client.SendText("found you!")
}

// Multi-device: get all connections for a user
clients := hub.GetClientsByUserID("user-123")
for _, c := range clients {
c.SendJSON(map[string]string{"type": "sync"})
}

// Count and list
count := hub.ClientCount()
allClients := hub.Clients()

Upgrade Options

Pass per-connection options to UpgradeConnection to configure the client before registration:

// Set user ID atomically during upgrade — before registration.
// This prevents the window where a client exists without a user ID,
// which could bypass MaxConnectionsPerUser limits.
client, err := hub.UpgradeConnection(w, r, wshub.WithUserID(userID))
if err != nil {
log.Printf("Upgrade failed: %v", err)
return
}

Drop Policy

Control what happens when a client's send buffer is full:

PolicyDescription
DropNewestDiscard the new message when the buffer is full (default)
DropOldestEvict the oldest queued message to make room for the new one
// Keep the most recent data (good for real-time state updates)
hub := wshub.NewHub(
wshub.WithDropPolicy(wshub.DropOldest),
wshub.WithHooks(wshub.Hooks{
OnSendDropped: func(client *wshub.Client, data []byte) {
log.Printf("Dropped message for slow client %s", client.ID)
},
}),
)

Health and Readiness

Drop-in HTTP handlers for Kubernetes /healthz and /readyz probes, plus low-level accessors for building custom health logic.

MethodDescription
HealthHandler()Returns an http.HandlerFunc for /healthz — 200 when alive, 503 otherwise
ReadyHandler()Returns an http.HandlerFunc for /readyz — 200 when alive and StateRunning, 503 otherwise
Health()Returns a HealthStatus snapshot (all reads are lock-free atomic loads)
Alive()True while the Run() goroutine is executing
Ready()True when alive and in StateRunning (accepting connections)
Uptime()Elapsed time since Run() started; zero before start or after exit

Both handlers respond with a JSON body containing alive, ready, state, uptime_ns, and clients.

// Register Kubernetes probes
http.HandleFunc("/healthz", hub.HealthHandler()) // liveness
http.HandleFunc("/readyz", hub.ReadyHandler()) // readiness

// Or use the snapshot for custom logic
h := hub.Health()
fmt.Printf("state=%s uptime=%s clients=%d\n", h.State, h.Uptime, h.Clients)

// Low-level accessors
if !hub.Alive() {
log.Println("Run() has not started or has exited")
}
if !hub.Ready() {
log.Println("Hub is draining or stopped — not accepting connections")
}

Graceful Drain

Drain stops accepting new connections (HTTP 503) while letting existing connections finish in-flight messages. Designed for zero-downtime rolling deploys (Kubernetes preStop, SIGTERM handlers).

MethodDescription
Drain(ctx)Stop accepting connections and wait for existing ones to disconnect (or the context to expire)
State()Returns the current HubState (StateRunning, StateDraining, or StateStopped)
IsRunning()True when the hub is accepting new connections
IsDraining()True when the hub is draining (no new connections, existing ones finishing)

During drain, idle connections whose send buffers have been empty for the drain timeout (default 30s, configurable via WithDrainTimeout) are proactively closed with CloseGoingAway (1001).

// Kubernetes preStop / SIGTERM handler
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()

hub.Drain(ctx) // stop new connections, wait for existing ones
hub.Shutdown(ctx) // force-close anything remaining

Hub State

StateDescription
StateRunningAccepting new connections and processing messages normally
StateDrainingNo new connections (HTTP 503), existing connections finishing in-flight messages
StateStoppedHub shut down, all connections closed, Run loop exited
// Health/readiness probes
func readinessHandler(w http.ResponseWriter, r *http.Request) {
if hub.IsRunning() {
w.WriteHeader(http.StatusOK)
} else {
w.WriteHeader(http.StatusServiceUnavailable)
}
}

// Check state directly
fmt.Println(hub.State()) // "running", "draining", or "stopped"

Graceful Shutdown

The hub supports context-aware graceful shutdown that closes all client connections:

ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()

// Two-phase shutdown for zero-downtime deploys:
// 1. Drain — stop new connections, let existing ones finish
if err := hub.Drain(ctx); err != nil {
log.Printf("Drain timeout: %v (forcing shutdown)", err)
}
// 2. Shutdown — force-close anything remaining
if err := hub.Shutdown(ctx); err != nil {
log.Printf("Shutdown error: %v", err)
}