Skip to main content

Centrifugo

Centrifugo is a real-time messaging server that provides WebSocket and WebTransport support with automatic fallback.

Production Ready

Centrifugo is the recommended transport for production deployments. It handles connection management, scaling, and provides built-in WebTransport with WebSocket fallback.

Overview

PropertyValue
ProtocolWebSocket (TCP) + WebTransport (UDP/QUIC)
DirectionBidirectional
Latency1-5ms (WebSocket) / 0.5-2ms (WebTransport)
ScalingHorizontal via Redis
Browser SupportAll browsers (WebSocket) / Chrome 97+, Firefox 114+ (WebTransport)

Architecture

┌─────────────────────────────────────────────────────────────────────────┐
│ CENTRIFUGO ARCHITECTURE │
├─────────────────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ │
│ │ Consumer │────▶│ Centrifugo │────▶│ Clients │ │
│ │ (Publisher) │ │ (Server) │ │ (Browsers) │ │
│ └─────────────────┘ └─────────────────┘ └─────────────────┘ │
│ │ │ │ │
│ │ HTTP API │ Redis │ WS/WT │
│ ▼ ▼ ▼ │
│ ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ │
│ │ /api/publish │ │ Pub/Sub │ │ /connection/ │ │
│ │ POST + X-API- │ │ + History │ │ websocket or │ │
│ │ Key │ │ │ │ webtransport │ │
│ └─────────────────┘ └─────────────────┘ └─────────────────┘ │
└─────────────────────────────────────────────────────────────────────────┘

Why Centrifugo?

FeatureNative WebSocketNative WebTransportCentrifugo
Auto ReconnectionManualManualBuilt-in
Fallback TransportNoNoYes (WT → WS)
History/RecoveryNoNoYes
Horizontal ScalingComplexComplexBuilt-in (Redis)
PresenceNoNoYes
Browser SupportAllLimitedAll

Endpoints

TransportURL
WebSocketwss://wt.datastream.hypetech.games/connection/websocket
WebTransporthttps://wt.datastream.hypetech.games/connection/webtransport
API (Internal)https://centrifugo:9000/api/publish

Kubernetes Deployment

ConfigMap

apiVersion: v1
kind: ConfigMap
metadata:
name: centrifugo-config
namespace: datastream
data:
config.json: |
{
"log": {
"level": "info"
},
"http_api": {
"key": "your-api-key-here"
},
"client": {
"insecure": true,
"allowed_origins": [
"https://datastream.hypetech.games",
"http://localhost:3000"
]
},
"channel": {
"without_namespace": {
"allow_subscribe_for_client": true,
"history_size": 100,
"history_ttl": "300s"
},
"namespaces": [
{
"name": "stream",
"allow_subscribe_for_client": true,
"allow_publish_for_client": false,
"history_size": 100,
"history_ttl": "300s"
}
]
},
"http_server": {
"port": 443,
"internal_port": 9000,
"tls": {
"enabled": true,
"cert_pem": "/certs/tls.crt",
"key_pem": "/certs/tls.key"
},
"http3": {
"enabled": true
}
},
"webtransport": {
"enabled": true
},
"engine": {
"type": "redis",
"redis": {
"address": "redis://redis:6379"
}
}
}

Deployment

apiVersion: apps/v1
kind: Deployment
metadata:
name: centrifugo
namespace: datastream
spec:
replicas: 1
selector:
matchLabels:
app: centrifugo
template:
metadata:
labels:
app: centrifugo
spec:
containers:
- name: centrifugo
image: centrifugo/centrifugo:v6
args:
- centrifugo
- --config=/config/config.json
ports:
- containerPort: 443
protocol: TCP
name: https
- containerPort: 443
protocol: UDP
name: webtransport
- containerPort: 9000
protocol: TCP
name: internal
volumeMounts:
- name: config
mountPath: /config
readOnly: true
- name: tls-certs
mountPath: /certs
readOnly: true
resources:
requests:
memory: "64Mi"
cpu: "50m"
limits:
memory: "256Mi"
cpu: "500m"
volumes:
- name: config
configMap:
name: centrifugo-config
- name: tls-certs
secret:
secretName: webtransport-tls

Services

# ClusterIP for internal communication
apiVersion: v1
kind: Service
metadata:
name: centrifugo
namespace: datastream
spec:
type: ClusterIP
selector:
app: centrifugo
ports:
- name: https
port: 443
targetPort: 443
protocol: TCP
- name: internal
port: 9000
targetPort: 9000
protocol: TCP
---
# NLB for external WebTransport access
apiVersion: v1
kind: Service
metadata:
name: centrifugo-wt
namespace: datastream
annotations:
service.beta.kubernetes.io/aws-load-balancer-type: "external"
service.beta.kubernetes.io/aws-load-balancer-nlb-target-type: "ip"
service.beta.kubernetes.io/aws-load-balancer-scheme: "internet-facing"
spec:
type: LoadBalancer
loadBalancerClass: service.k8s.aws/nlb
selector:
app: centrifugo
ports:
- name: https
port: 443
targetPort: 443
protocol: TCP
- name: quic
port: 443
targetPort: 443
protocol: UDP

TLS Certificate

Create a certificate for the WebTransport domain using cert-manager:

apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
name: webtransport-tls
namespace: datastream
spec:
secretName: webtransport-tls
issuerRef:
name: letsencrypt-prod
kind: ClusterIssuer
dnsNames:
- wt.datastream.hypetech.games

Client Implementation

JavaScript (centrifuge-js)

<script src="https://unpkg.com/centrifuge@5/dist/centrifuge.js"></script>
<script>
const CENTRIFUGO_URL = 'wss://wt.datastream.hypetech.games/connection/websocket';
const CENTRIFUGO_WT_URL = 'https://wt.datastream.hypetech.games/connection/webtransport';

async function connect() {
const hasWebTransport = typeof WebTransport !== 'undefined';

// Configure transports with fallback
const transports = [];

// Try WebTransport first if available
if (hasWebTransport) {
transports.push({
transport: 'webtransport',
endpoint: CENTRIFUGO_WT_URL
});
}

// Always add WebSocket as fallback
transports.push({
transport: 'websocket',
endpoint: CENTRIFUGO_URL
});

const centrifuge = new Centrifuge(transports);

centrifuge.on('connecting', (ctx) => {
console.log('Connecting...');
});

centrifuge.on('connected', (ctx) => {
console.log(`Connected via ${ctx.transport}`);
});

centrifuge.on('disconnected', (ctx) => {
console.log('Disconnected');
});

centrifuge.on('error', (ctx) => {
console.error('Error:', ctx);
});

centrifuge.connect();

return centrifuge;
}

function subscribe(centrifuge, channel, onMessage) {
const sub = centrifuge.newSubscription(channel);

sub.on('publication', (ctx) => {
onMessage(ctx.data);
});

sub.on('subscribed', (ctx) => {
console.log(`Subscribed to ${channel}`);
});

sub.on('error', (ctx) => {
console.error(`Subscription error for ${channel}:`, ctx);
});

sub.subscribe();
return sub;
}

// Usage
const centrifuge = await connect();
subscribe(centrifuge, 'stream:crash', (data) => {
console.log('Round result:', data);
});
</script>

React Hook

import { useState, useEffect, useCallback } from 'react';
import { Centrifuge } from 'centrifuge';

const CENTRIFUGO_URL = 'wss://wt.datastream.hypetech.games/connection/websocket';
const CENTRIFUGO_WT_URL = 'https://wt.datastream.hypetech.games/connection/webtransport';

function useCentrifugo(channels) {
const [data, setData] = useState({});
const [connected, setConnected] = useState(false);
const [transport, setTransport] = useState(null);
const [error, setError] = useState(null);

useEffect(() => {
const hasWebTransport = typeof WebTransport !== 'undefined';
const transports = [];

if (hasWebTransport) {
transports.push({ transport: 'webtransport', endpoint: CENTRIFUGO_WT_URL });
}
transports.push({ transport: 'websocket', endpoint: CENTRIFUGO_URL });

const centrifuge = new Centrifuge(transports);

centrifuge.on('connected', (ctx) => {
setConnected(true);
setTransport(ctx.transport);
});

centrifuge.on('disconnected', () => {
setConnected(false);
});

centrifuge.on('error', (ctx) => {
setError(ctx.error?.message || 'Unknown error');
});

// Subscribe to channels
const subscriptions = channels.map(channel => {
const sub = centrifuge.newSubscription(channel);
sub.on('publication', (ctx) => {
setData(prev => ({
...prev,
[channel]: ctx.data
}));
});
sub.subscribe();
return sub;
});

centrifuge.connect();

return () => {
subscriptions.forEach(sub => sub.unsubscribe());
centrifuge.disconnect();
};
}, [channels.join(',')]);

return { data, connected, transport, error };
}

// Usage
function GameDashboard() {
const { data, connected, transport, error } = useCentrifugo([
'stream:crash',
'stream:double',
'stream:aviador'
]);

if (error) return <div>Error: {error}</div>;

return (
<div>
<div>Status: {connected ? `Connected (${transport})` : 'Disconnected'}</div>
{Object.entries(data).map(([channel, result]) => (
<div key={channel}>
<h3>{channel}</h3>
<pre>{JSON.stringify(result, null, 2)}</pre>
</div>
))}
</div>
);
}

Server-Side Publishing (Go)

Publisher Adapter

package centrifugo

import (
"bytes"
"context"
"crypto/tls"
"encoding/json"
"fmt"
"net/http"
"time"
)

type Publisher struct {
baseURL string
apiKey string
httpClient *http.Client
}

type PublishRequest struct {
Channel string `json:"channel"`
Data json.RawMessage `json:"data"`
}

func NewPublisher(baseURL, apiKey string) *Publisher {
transport := &http.Transport{
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
}
return &Publisher{
baseURL: baseURL,
apiKey: apiKey,
httpClient: &http.Client{
Timeout: 5 * time.Second,
Transport: transport,
},
}
}

func (p *Publisher) Publish(ctx context.Context, channel string, data []byte) error {
req := PublishRequest{
Channel: channel,
Data: data,
}

body, err := json.Marshal(req)
if err != nil {
return fmt.Errorf("marshal request: %w", err)
}

url := fmt.Sprintf("%s/api/publish", p.baseURL)
httpReq, err := http.NewRequestWithContext(ctx, http.MethodPost, url, bytes.NewReader(body))
if err != nil {
return fmt.Errorf("create request: %w", err)
}

httpReq.Header.Set("Content-Type", "application/json")
httpReq.Header.Set("X-API-Key", p.apiKey)

resp, err := p.httpClient.Do(httpReq)
if err != nil {
return fmt.Errorf("http request: %w", err)
}
defer resp.Body.Close()

if resp.StatusCode != http.StatusOK {
return fmt.Errorf("centrifugo error: status %d", resp.StatusCode)
}

return nil
}

Environment Variables

VariableDescriptionExample
CENTRIFUGO_URLInternal API URLhttps://centrifugo:9000
CENTRIFUGO_API_KEYAPI key for publishingyour-api-key-here

Channel Naming

Channels follow the format: namespace:channel_name

ChannelDescription
stream:crashCrash game results
stream:doubleDouble game results
stream:aviadorAviador game results
stream:type:multiplierAll multiplier-type games
stream:type:rouletteAll roulette-type games

Configuration Reference

Centrifugo v6 Config Keys

KeyDescriptionDefault
log.levelLog level (debug, info, warn, error)info
http_api.keyAPI key for HTTP APIRequired
client.insecureAllow anonymous connectionsfalse
client.allowed_originsCORS origins[]
channel.without_namespace.allow_subscribe_for_clientAllow client subscriptionsfalse
channel.namespaces[].nameNamespace name-
channel.namespaces[].allow_subscribe_for_clientAllow subscriptionsfalse
http_server.portMain server port (TLS)8000
http_server.internal_portInternal API port9000
http_server.tls.enabledEnable TLSfalse
http_server.http3.enabledEnable HTTP/3/QUICfalse
webtransport.enabledEnable WebTransportfalse
engine.typeEngine type (memory, redis)memory
engine.redis.addressRedis URL-

AWS NLB Limitations

UDP Support

AWS NLB Load Balancer Controller may not create UDP listeners automatically when both TCP and UDP are on the same port. WebTransport requires UDP for QUIC.

Current Workaround: The client automatically falls back to WebSocket (TCP) when WebTransport fails.

Troubleshooting

"unknown channel" Error

Cause: Channel namespace not configured

Solution: Add namespace to config:

{
"channel": {
"namespaces": [
{
"name": "stream",
"allow_subscribe_for_client": true
}
]
}
}

"Origin not authorized" Error

Cause: Client origin not in allowed_origins

Solution: Add origin to config:

{
"client": {
"allowed_origins": [
"https://yourdomain.com"
]
}
}

WebTransport Handshake Failed

Cause: Usually UDP not available (NLB limitation)

Solution: Client automatically falls back to WebSocket. For true WebTransport support, use a platform with full UDP support (bare metal, EC2 without NLB).

API Returns 400 Bad Request

Cause: Internal API port using TLS in v6

Solution: Use HTTPS for internal API:

https://centrifugo:9000/api/publish

Performance

MetricWebSocketWebTransport
Connection time1 RTT0-1 RTT
Message latency1-5ms0.5-2ms
Head-of-line blockingYesNo
Browser supportAllChrome/Firefox only

Resources