CE

Centrifugo

Scalable real-time messaging server for apps and websites

Chat & Communication ★ 10.8k stars Medium setup MIT

Centrifugo is a real-time messaging server that handles WebSocket and other transports to push live updates to web and mobile applications. It is language-agnostic and integrates with existing backends over an API.

Key features

  • WebSocket messaging
  • Horizontal scaling
  • Language-agnostic API
  • Channel presence

Pros & cons

Strengths

  • Handles many connections
  • Easy to integrate

Trade-offs

  • Not a chat app itself
  • Requires backend integration

Centrifugo replaces

Last reviewed Aug 26, 2026 · 874 words

Your dashboard polls every 5 seconds and you want it to update the instant something changes; that is the whole reason Centrifugo exists. It is a single Go binary that holds thousands of WebSocket connections open, lets your backend publish a JSON message to a channel with 1 HTTP call, and delivers that message to every subscribed browser and phone within milliseconds. It is not a chat application, it has no user accounts, and it shows nobody anything on its own. It is plumbing, and it is the best plumbing of its kind you can self-host.

The job it does and the job it refuses

Centrifugo owns exactly 1 hard problem: keeping many long-lived client connections alive, authenticated, and subscribed, across reconnects and across server restarts. Your backend keeps everything else: who the users are, what they are allowed to see, and where the data lives. When something happens, your code publishes to a channel; Centrifugo fans it out. If you were hoping for a Slack replacement, it is the wrong category, and Mattermost is the right one. If you are building or extending your own web app and want live updates without writing WebSocket handling and reconnection logic yourself, this is the tool.

One binary, one generated config, port 8000

Do not copy a config from a blog post, including this one; key names have moved between major versions. Let the binary write a valid one for the version you pulled:

docker run --rm -v "$PWD":/centrifugo centrifugo/centrifugo:latest centrifugo genconfig
docker run -d --name centrifugo -p 8000:8000 \
  -v "$PWD/config.json":/centrifugo/config.json \
  centrifugo/centrifugo:latest centrifugo -c config.json

genconfig produces a file with fresh random secrets: the HMAC key clients' tokens are signed with, the API key your backend uses, and the admin credentials. Enable the admin web UI in that file and open port 8000 in a browser: you get connection counts, channel stats, and a form to publish test messages, which is how you confirm the whole loop works before writing a line of application code. Memory at idle is well under the catalogue's 256 MB.

Publishing from any language is 1 HTTP request:

curl -X POST http://localhost:8000/api/publish \
  -H "X-API-Key: $CENTRIFUGO_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"channel": "sensors", "data": {"room": "office", "temp_c": 21.5}}'

And subscribing in a browser with the official JavaScript client is a handful of lines:

const c = new Centrifuge("wss://rt.example.com/connection/websocket", { token });
c.newSubscription("sensors")
  .on("publication", (ctx) => render(ctx.data))
  .subscribe();
c.connect();

There are equivalent SDKs for Swift, Kotlin and Java, Dart, Go, and Python, all sharing the same protocol and reconnect behaviour.

Tokens: your app mints them, Centrifugo checks them

The token above is a JWT your backend signs with the shared HMAC secret, carrying the user ID in sub and an expiry. Centrifugo verifies the signature and knows who is connected without ever calling you. Channel permissions are declared per namespace in the config: which channels allow anonymous subscribers (a public dashboard), which keep presence lists, which retain a short history so a reconnecting client catches up on what it missed. For anything more dynamic, proxy mode has Centrifugo call your backend over HTTP or gRPC on connect or subscribe and let you say yes or no. That is the pattern that keeps the authorisation logic in 1 place, your app, rather than duplicating it in a config file.

Scaling and where it stops

The default in-memory engine is a single node and is enough for tens of thousands of concurrent connections on a small VM, which is far more than any homelab or internal tool will see. When you need several nodes, or you want channel history to survive a restart, you switch the engine to Redis and run as many Centrifugo instances behind a load balancer as you like. Transports beyond WebSocket (HTTP streaming, server-sent events, WebTransport) are there for clients behind hostile proxies. What it does not do is store your data, queue messages for offline devices, or deliver push notifications to a locked phone; for that last one you want a mobile push service or ntfy.

The neighbours, so you pick the right one

soketi speaks the Pusher protocol; if you already have pusher-js in your code, it is the drop-in and Centrifugo is not. EMQX is MQTT, which is what sensors and microcontrollers speak, and it sits before Centrifugo in an IoT stack rather than instead of it. ntfy is for telling a human something on their phone. Centrifugo is for telling a running web page something right now.

What I'd do

Any self-built app with a "live" screen: Centrifugo in a container with the memory engine, genconfig output committed to a secrets store, JWTs minted by the backend, and 1 namespace per feature so permissions stay legible. Expose it under its own hostname behind Caddy with WebSocket passthrough, which Caddy does without configuration. Leave Redis out until the day you run a second node; for most self-hosters that day never comes, and the single 20 MB binary keeps running.

Compare Centrifugo

2 head-to-head comparisons.

Similar chat & communication apps