The WebSocket Nightmare as Systems Scale
When I first started building chat features or real-time notifications, I usually opted for a “home-grown” solution: using the gorilla/websocket library in Go. It ran perfectly on my local machine with just a few users. However, once the system hit the 10,000 concurrent user mark, the real trouble began to surface.
The biggest issue with pure WebSockets lies in State. Each connection is a continuous pipe between the client and the server. If you run 3-4 servers to balance the load, how does server A know a user is on server B to send a message? You’d have to build an additional Pub/Sub layer using Redis for synchronization. Not to mention handling automatic reconnections, channel management, or JWT security, which are incredibly time-consuming. Instead of focusing on features, you end up bogged down in maintaining WebSocket infrastructure.
Three Common Approaches for Real-time Systems
Here are the options engineers typically consider when scaling a system:
- Self-built (Go + Redis): You have 100% control over the code but expend massive effort handling bugs that arise during horizontal scaling.
- Using Cloud Services (Firebase, Pusher): Super fast deployment. However, the end-of-month bill can reach thousands of dollars if user volume spikes, along with the concern of vendor lock-in.
- Centrifugo: A specialized solution that stands independently as its own server. It handles everything from connections and Pub/Sub to scalability. Your backend (Go, Python, Node.js) simply calls an API.
Centrifugo is the perfect middle ground. It offers the high performance of Go while being as easy to integrate as paid Cloud services.
Why Centrifugo is a Top Choice for Production?
Written in Go, Centrifugo takes full advantage of concurrency. Its most valuable feature is that it completely decouples real-time logic from your application server.
The data model changes to:
Client <–> Centrifugo <– (API/GRPC) –> Go Backend
With this model, the Go backend becomes “stateless.” It only handles authentication and pushing messages. Centrifugo takes the heavy RAM and CPU burden of maintaining millions of connections off your shoulders.
Practical Implementation Guide
1. Initializing Centrifugo with Docker
To get started as quickly as possible, I’ll use Docker. You just need a config.json file to set up your security secret keys.
# Create a quick configuration file
echo '{"token_hmac_secret_key": "my-secret", "api_key": "my-api-key"}' > config.json
# Run Centrifugo with a single command
docker run -p 8000:8000 -v `pwd`/config.json:/centrifugo/config.json centrifugo/centrifugo centrifugo
Once running, you can immediately access the administrative dashboard at http://localhost:8000.
2. Backend: Authorization and Sending Messages
Clients wanting to connect to Centrifugo need an authentication token (JWT). The Go backend is responsible for generating this token. During development, I often use toolcraft.app to quickly check the returned JSON structure, making debugging much smoother.
func GenerateCentrifugoToken(userID string) (string, error) {
claims := jwt.MapClaims{
"sub": userID,
"exp": time.Now().Add(time.Hour * 24).Unix(),
}
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
return token.SignedString([]byte("my-secret"))
}
To send notifications from the server to the client, simply call a basic API via the gocent library:
c := gocent.New(gocent.Config{
Addr: "http://localhost:8000/api",
Key: "my-api-key",
})
// Send a message to the "notifications" channel
ctx := context.Background()
_, err := c.Publish(ctx, "notifications", []byte(`{"message": "Hello everyone!"}`))
3. Frontend: Receiving Real-time Messages
On the client side, the centrifuge-js library is lightweight and easy to use. It automatically handles complex tasks like reconnecting when the network drops.
const centrifuge = new Centrifuge('ws://localhost:8000/connection/websocket', {
token: 'JWT_FROM_BACKEND'
});
const sub = centrifuge.newSubscription('notifications');
sub.on('publication', (ctx) => {
console.log('New message:', ctx.data.message);
});
sub.subscribe();
centrifuge.connect();
Hard-Won Lessons from Production Operations
When scaling your system, keep these 3 key points in mind:
Always Use the Redis Engine
By default, Centrifugo stores data in memory. To run multiple Centrifugo nodes behind a Load Balancer, you must use Redis as a bridge. Otherwise, a message sent to one node won’t reach a user connected to a different node.
Proxy Authentication
If JWTs aren’t flexible enough, try Proxy Authentication. Every time a user connects, Centrifugo will ask the Go backend: “Is this user valid?”. This allows for extremely tight access control based on your specific business logic.
Closely Monitor Metrics
Enable the /metrics endpoint and integrate it with Grafana. Pay close attention to num_clients (number of connections) and messages_sent. If you see CPU spikes when connections exceed 100,000, it’s time to scale up with more Centrifugo nodes.
Building a production-grade WebSocket system from scratch is a difficult and costly endeavor. By combining Centrifugo with Go, you’ll have a robust infrastructure that allows you to focus entirely on developing features for your users.

