Skip to content

Latest commit

 

History

183 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

wshub

wshub

A production-ready, scalable WebSocket package for Go with support for rooms, broadcasting, multi-node clustering, middleware, hooks, and extensibility.

Go Reference Latest release Go version CI Coverage License: MIT

Documentation · API Reference · Changelog

Two browser tabs joining a room and chatting in real time, built on wshub's rooms and broadcasting API

The room-based chat example — `go run ./examples/chat`

Why wshub?

A raw gorilla/websocket (or nhooyr.io/websocket) connection gets you a socket. Everything past that — the parts that turn "I can send a frame" into "I can run this in production" — is what wshub provides:

Capability wshub Raw WebSocket library
Connection registry, rooms, broadcasting You build it
Backpressure & drop policies You build it
Graceful drain + shutdown You build it
Multi-node scaling (Redis / NATS) You build it
Rate limiting (conns, rooms, messages) You build it
Metrics + official Prometheus subpackage You build it
/healthz / /readyz probes You build it
Lifecycle hooks & middleware chain You build it

wshub isn't a replacement for a WebSocket protocol library — it's built on top of gorilla/websocket. It's the infrastructure layer around the socket that most realtime services end up writing themselves, packaged once, benchmarked, and kept production-safe by default.

Performance

SendToClient       105 ns/op    0 allocs   (100K clients)
Broadcast            22.6 ms    0 allocs   (100K clients)
Handshake rate     36,891 conn/s            (10K connections)
Fanout             ~499K msg/s              (5K clients, single broadcaster)

SendToClient and Broadcast are in-process benchmarks: reproduce with go test -bench=. -benchmem ./.... Handshake rate and fanout are end-to-end load tests: reproduce with make loadtest LOADTEST_ARGS="-scenario connect -clients 10000" and make loadtest LOADTEST_ARGS="-scenario fanout -clients 5000" respectively — full methodology and numbers in Benchmarks below.

Features

  • Production-Ready: Proper concurrency, graceful shutdown & drain, error handling
  • Horizontally Scalable: Multi-node support via adapter pattern (Redis, NATS, or custom)
  • Pluggable: Bring your own logger, metrics
  • Middleware System: Chain handlers with custom logic
  • Lifecycle Hooks: Hook into connection, message, room, and backpressure events
  • Room Support: Group clients into rooms for targeted broadcasting
  • Metrics & Logging: Built-in interfaces for observability; official Prometheus subpackage (wshub/prometheus)
  • Configurable: Extensive configuration with builder pattern
  • Limits & Rate Limiting: Control connections, rooms, and message rates
  • Backpressure Control: Configurable drop policies with notification hooks
  • Write Coalescing: Opt-in batching of text messages into single frames for reduced syscalls
  • Health Probes: Built-in /healthz and /readyz handlers with JSON responses for Kubernetes
  • Global Counts: Cluster-wide client and room counts via presence gossip
  • Zero Business Logic: Pure infrastructure, bring your own logic

Installation

go get github.com/KARTIKrocks/wshub

Quick Start

package main

import (
    "context"
    "log"
    "net/http"
    "os"
    "os/signal"
    "syscall"
    "time"

    "github.com/KARTIKrocks/wshub"
)

func main() {
    hub := wshub.NewHub(
        wshub.WithMessageHandler(func(client *wshub.Client, msg *wshub.Message) error {
            log.Printf("Message from %s: %s", client.ID, msg.Text())
            return client.Send(msg.Data) // echo back
        }),
    )

    go hub.Run()

    http.HandleFunc("/ws", hub.HandleHTTP())
    http.HandleFunc("/healthz", hub.HealthHandler())
    http.HandleFunc("/readyz", hub.ReadyHandler())

    srv := &http.Server{Addr: ":8080"}
    go func() {
        log.Println("Listening on :8080")
        if err := srv.ListenAndServe(); err != http.ErrServerClosed {
            log.Fatal(err)
        }
    }()

    quit := make(chan os.Signal, 1)
    signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
    <-quit

    // Two-phase shutdown for zero-downtime deploys
    ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
    defer cancel()
    hub.Drain(ctx)    // stop new connections, let existing ones finish
    hub.Shutdown(ctx) // force-close anything remaining
    srv.Shutdown(ctx)
}

Note: since v1.7.0 the default origin check is AllowSameOrigin. If your front-end is served from a different origin than the WebSocket endpoint, allowlist it with wshub.AllowOrigins(...) — see Configuration → Origin Checking.

Documentation

Full guides live at kartikrocks.github.io/wshub:

Guide Covers
Getting Started Install and run a minimal server
Hub Broadcasting, client lookup, drain, health probes, shutdown
Client Per-connection sending, metadata, callbacks
Messages Message type, handlers, zero-alloc JSON fan-out
Rooms Joining, room broadcasting, queries
Middleware Built-in and custom middleware chains
Router Event-based message dispatch
Hooks Connection, message, and room lifecycle hooks
Adapters Multi-node scaling via Redis or NATS
Presence Cluster-wide client and room counts
Configuration Buffers, timeouts, compression, origin checking
Limits Connection, room, and rate limits
Metrics Collector interface and the Prometheus subpackage
Errors Sentinel errors and errors.Is matching

Exact type signatures are generated from source on pkg.go.dev.

Runnable programs are in examples/ — simple, chat, auth, notifications, metrics, and multinode.

Benchmarks

Two kinds of numbers below:

  1. In-process dispatch (Go benchmarks with mock clients) — measures hub bookkeeping and channel push cost. Useful for spotting allocation regressions, not for predicting real throughput.
  2. End-to-end load tests (real httptest.Server + gorilla/websocket dialer) — measures what an actual deployment will see.

Measured on an Intel i5-11400H @ 2.70GHz (12 cores), Go 1.27, Linux.

Run them yourself:

go test -bench=. -benchmem ./...      # in-process micro-benchmarks
make loadtest LOADTEST_ARGS="..."     # end-to-end load tests

In-process dispatch (mock clients)

These measure how fast the hub iterates its snapshot and pushes to client channels. They do not include TCP, writePump, or remote-client work.

Broadcast dispatch (zero allocations)

Operation Clients Time Allocs
Broadcast 100,000 22.6 ms 0
Broadcast 1,000,000 269 ms 0
BroadcastToRoom 100,000 25.6 ms 0
BroadcastToRoom 1,000,000 293 ms 0
BroadcastExcept 100,000 23.4 ms 1
BroadcastExcept 1,000,000 274 ms 1
BroadcastToRoomExcept 100,000 23.0 ms 1
BroadcastToRoomExcept 1,000,000 269 ms 1

Targeted Send (O(1) at any scale, zero allocations)

Operation Scale Time Allocs
SendToClient 100,000 clients 105 ns 0
SendToClient 1,000,000 clients 112 ns 0
SendToUser 100,000 users 167 ns 1
SendToUser 1,000,000 users 166 ns 1

Global Counts — Presence (zero allocations)

Operation Nodes Time Allocs
GlobalClientCount 5 55.1 ns 0
GlobalClientCount 50 360 ns 0
GlobalClientCount 100 684 ns 0
GlobalClientCount 500 3.82 μs 0
GlobalRoomCount 5 111 ns 0
GlobalRoomCount 50 892 ns 0
GlobalRoomCount 100 1.54 μs 0
GlobalRoomCount 500 9.16 μs 0

Client & Room Lookups (zero allocations)

Operation Time Allocs
GetClient (1,000 clients) 16.5 ns 0
ClientCount 0.25 ns 0
GetClientByUserID 45.8 ns 0
RoomExists 15.6 ns 0
RoomCount 15.0 ns 0
GetMetadata 16.7 ns 0
SetMetadata 28.0 ns 0

Client Send

Operation Time Allocs
Send (text) 61.5 ns 1
SendJSON 501 ns 5

Middleware Chain

Mode Time Allocs
Built (cached) 12.5 ns 0
Unbuilt (on-the-fly) 12.4 ns 0

Real-world load tests

End-to-end timings using real WebSocket connections via httptest.Server and gorilla/websocket.Dialer. Latency is measured by embedding a unix-nano timestamp in the payload and computing now - sent on receive. Reproduce with make loadtest.

Connect — handshake throughput

Clients Connect time Rate Mem/conn
1,000 59 ms 15,754 conn/s 27.1 KB
5,000 162 ms 29,853 conn/s 24.0 KB
10,000 263 ms 36,891 conn/s 25.9 KB

Fanout — single broadcaster, 100 msg/s for 10s, 128 B payload

Clients Throughput p50 p95 p99
1,000 100,000 msg/s 1.48 ms 1.85 ms 2.75 ms
5,000 499,500 msg/s 7.91 ms 19.0 ms 31.8 ms
10,000 693,900 msg/s 1.72 s 3.19 s 3.34 s

Past ~5K clients on a single node, fanout latency grows steeply — the bottleneck is Go scheduler pressure across 3 × clients goroutines (readPump + writePump

  • handshake server), not the hub's dispatch loop. For higher per-node fanout, tune SendChannelSize, enable CoalesceWrites, or scale horizontally.

Rooms — broadcast scoped to a room (100 msg/s, 10s)

Clients Rooms Per-room p50 p99
5,000 100 5.68 ms 7.88 ms
10,000 100 12.29 ms 19.01 ms

Echo — per-connection round-trip (5,000 clients, 10s)

RTT/sec p50 p95 p99
318,348 14.4 ms 24.1 ms 48.0 ms

Note on WithParallelBroadcast: in real load tests, parallel dispatch is consistently slower than the default serial path because the per-call cost of trySend (RLock + defer/recover) dominates and parallel batching can't overcome it. The option remains for backward compatibility but is no longer recommended — use the default serial broadcast.

Always call Build() on your middleware chain for best performance.

Concurrent Access (parallel goroutines)

Operation Time Allocs
GetClient 24.7 ns 0
ClientCount 0.17 ns 0
Metadata (set+get) 66.0 ns 0
Broadcast (100 clients) 4.4 μs 121

Message Creation

Operation Time Allocs
NewMessage 28.3 ns 0
NewTextMessage 28.2 ns 0
NewBinaryMessage 28.1 ns 0
NewJSONMessage 773 ns 8
NewRawJSONMessage 28.3 ns 0

Thread Safety

All Hub and Client methods are thread-safe. The package uses:

  • RWMutex for client/room maps
  • Separate mutexes for callbacks
  • Channels for cross-goroutine communication
  • WaitGroups for graceful shutdown

Security

wshub sits on the network edge — it terminates untrusted WebSocket upgrades — so the security surface is treated as part of the API, not an afterthought. v1.7.0 changed DefaultConfig() from AllowAllOrigins to AllowSameOrigin as a breaking change, because a default that accepts an upgrade from any origin leaves every server built on it open to cross-site WebSocket hijacking.

Every push and pull request to main is scanned by CodeQL, with a full re-scan weekly to catch newly published query patterns against unchanged code. govulncheck gates every merge on advisories that are reachable from this code's call graph. Both run separately against each of the four modules — the root package, prometheus, adapter/redis, and adapter/nats — because a scan started from the root stops at the nested go.mod boundaries and would miss the adapters' own dependency trees. Dependabot tracks updates across all four, plus the docs site and the GitHub Actions themselves.

See SECURITY.md for supported versions, what is in scope, and how to report a vulnerability privately.

License

MIT

Contributing

Contributions welcome! Please read CONTRIBUTING.md for guidelines.

About

A production-grade WebSocket hub for Go with rooms, broadcasting, backpressure, observability, graceful shutdown, and horizontal scaling. Zero business logic, pure infrastructure.

Topics

Resources

Contributing

Security policy

Stars

3 stars

Watchers

1 watching

Forks

Releases

Sponsor this project

Packages

Contributors

Languages