Centrifugo
Centrifugo is a real-time messaging server that provides WebSocket and WebTransport support with automatic fallback.
Centrifugo is the recommended transport for production deployments. It handles connection management, scaling, and provides built-in WebTransport with WebSocket fallback.
Overview
| Property | Value |
|---|---|
| Protocol | WebSocket (TCP) + WebTransport (UDP/QUIC) |
| Direction | Bidirectional |
| Latency | 1-5ms (WebSocket) / 0.5-2ms (WebTransport) |
| Scaling | Horizontal via Redis |
| Browser Support | All 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?
| Feature | Native WebSocket | Native WebTransport | Centrifugo |
|---|---|---|---|
| Auto Reconnection | Manual | Manual | Built-in |
| Fallback Transport | No | No | Yes (WT → WS) |
| History/Recovery | No | No | Yes |
| Horizontal Scaling | Complex | Complex | Built-in (Redis) |
| Presence | No | No | Yes |
| Browser Support | All | Limited | All |
Endpoints
| Transport | URL |
|---|---|
| WebSocket | wss://wt.datastream.hypetech.games/connection/websocket |
| WebTransport | https://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
| Variable | Description | Example |
|---|---|---|
CENTRIFUGO_URL | Internal API URL | https://centrifugo:9000 |
CENTRIFUGO_API_KEY | API key for publishing | your-api-key-here |
Channel Naming
Channels follow the format: namespace:channel_name
| Channel | Description |
|---|---|
stream:crash | Crash game results |
stream:double | Double game results |
stream:aviador | Aviador game results |
stream:type:multiplier | All multiplier-type games |
stream:type:roulette | All roulette-type games |
Configuration Reference
Centrifugo v6 Config Keys
| Key | Description | Default |
|---|---|---|
log.level | Log level (debug, info, warn, error) | info |
http_api.key | API key for HTTP API | Required |
client.insecure | Allow anonymous connections | false |
client.allowed_origins | CORS origins | [] |
channel.without_namespace.allow_subscribe_for_client | Allow client subscriptions | false |
channel.namespaces[].name | Namespace name | - |
channel.namespaces[].allow_subscribe_for_client | Allow subscriptions | false |
http_server.port | Main server port (TLS) | 8000 |
http_server.internal_port | Internal API port | 9000 |
http_server.tls.enabled | Enable TLS | false |
http_server.http3.enabled | Enable HTTP/3/QUIC | false |
webtransport.enabled | Enable WebTransport | false |
engine.type | Engine type (memory, redis) | memory |
engine.redis.address | Redis URL | - |
AWS NLB Limitations
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
| Metric | WebSocket | WebTransport |
|---|---|---|
| Connection time | 1 RTT | 0-1 RTT |
| Message latency | 1-5ms | 0.5-2ms |
| Head-of-line blocking | Yes | No |
| Browser support | All | Chrome/Firefox only |