Building a Self-Hosted WebRTC SFU Cluster with Media Servers for Scalable Video Conferencing
Quick Summary (TL;DR)
- An SFU (Selective Forwarding Unit) forwards individual media streams instead of mixing them—ideal for scalable group video.
- Move from a single SFU to a cluster when you exceed ~100–500 participants per node or need geographic distribution.
- Choose Mediasoup for raw performance and fine-grained control, or Janus for batteries-included plugins and faster prototyping.
- Load balance signaling over TCP/TLS and media over UDP—they need different strategies (HAProxy, Nginx, GeoDNS).
- Scale horizontally with stateless signaling + stateful media nodes, and monitor CPU, bandwidth, packet loss, jitter, and RTT.
Self-hosting a WebRTC SFU cluster is one of the most rewarding—and most misunderstood—projects in real-time communication. You get full control over media routing, costs, and privacy, but you also inherit the hard parts: UDP load balancing, geographic routing, and stateful media servers that don’t scale like ordinary web apps. This guide walks you through the architecture, tooling, and operational practices needed to run a production-grade cluster.
Introduction to WebRTC SFU Architecture
What is an SFU and Why Use It?
A Selective Forwarding Unit (SFU) is a media server that receives each participant’s stream and selectively forwards it to other participants. Unlike an MCU (Multipoint Control Unit), which decodes and mixes all streams into one composite feed, an SFU never touches the media payload—it just routes RTP packets.
This design has three big advantages:
- Low server CPU — no transcoding or mixing.
- Flexible layouts — clients decide which streams to render.
- Better quality — you can use simulcast/SVC to send low-res streams to constrained peers.
The trade-off is bandwidth: each client uploads once but downloads N−1 streams (or fewer with simulcast).
When to Move from a Single SFU to a Cluster
A single well-tuned SFU handles roughly 100–500 participants depending on resolution, bitrate, and hardware. You should cluster when you hit any of these signals:
- CPU saturation on media nodes during peak hours.
- Participants spread across continents experiencing high RTT.
- A single point of failure is unacceptable for your SLA.
- Conferences exceed a few hundred concurrent users.
flowchart TD
A[Single SFU] --> B{Concurrent users > 300?}
B -- No --> A
B -- Yes --> C{Multi-region users?}
C -- No --> D[Vertical scaling]
C -- Yes --> E[Cluster + Geo-routing]
D --> F{CPU still saturated?}
F -- Yes --> E
F -- No --> A
Choosing Your Media Server: Janus vs. Mediasoup
Janus: Modular and Feature-Rich
Janus is a C-based, plugin-driven SFU. Its plugin architecture (VideoRoom, Streaming, SIP, Record&Play) means you can ship a working conferencing app in days. It’s mature, well-documented, and has a large community. The downside: the plugin model can feel rigid when you need custom routing logic.
Mediasoup: Performance and Flexibility
Mediasoup is a Node.js library wrapping a C++ media core. You embed it directly into your application, giving you full control over routers, transports, and producers/consumers. It’s extremely efficient per CPU core and ideal for custom topologies—but you build more of the signaling and orchestration yourself.
Comparison Table: Janus vs. Mediasoup
| Feature | Janus | Mediasoup |
|---|---|---|
| Language core | C | C++ |
| Integration model | Plugin server | Library (Node.js) |
| Learning curve | Low–medium | Medium–high |
| Performance per core | Good | Excellent |
| Simulcast / SVC | Yes | Yes (advanced) |
| Custom routing | Via plugins | Fully programmable |
| Best for | Fast prototyping, standard apps | Custom, high-scale platforms |
Designing the Cluster Architecture
Core Components
A production cluster typically has four layers:
- Signaling servers — stateless WebSocket/HTTP services handling SDP exchange, room state, and auth.
- Media servers (SFUs) — stateful nodes that own RTP sessions.
- Load balancers — separate paths for signaling (TCP) and media (UDP).
- Coordination layer — Redis/etcd for room-to-SFU mapping and health.
graph LR
C1[Client] -->|HTTPS/WSS| LB1[Signaling LB]
C1 -->|SRTP/UDP| LB2[Media LB]
LB1 --> S1[Signaling Node 1]
LB1 --> S2[Signaling Node 2]
LB2 --> M1[SFU Node 1]
LB2 --> M2[SFU Node 2]
S1 --> R[(Redis / etcd)]
S2 --> R
M1 --> R
M2 --> R
Media Routing and Cascading
When a conference spans regions, you have two options:
- Single-region SFU — all participants connect to one node. Simple, but high RTT for distant users.
- Cascading — SFUs in different regions forward streams to each other. Lower latency per region, but more bandwidth between data centers.
Cascading topologies include mesh (every SFU peers with every other) and tree (a primary SFU fans out to secondaries). For most deployments, a tree with a designated primary per conference balances bandwidth and complexity.
Load Balancing Strategies for WebRTC SFU Clusters
Signaling and Media Load Balancing
These are fundamentally different problems:
- Signaling is TCP/TLS, short-lived, and can use standard Layer 7 balancing (Nginx, HAProxy, Envoy) with sticky sessions.
- Media is UDP, long-lived, and requires UDP-aware balancing. HAProxy in
mode tcpwithbalance leastconnworks well, as does Nginx’sstreammodule with UDP support.
A critical rule: once a client is assigned to an SFU, it must stay there for the session. Use consistent hashing on room ID or a coordination service that pins rooms to nodes.
Geographic and Latency-Based Routing
Use GeoDNS (e.g., Route 53 latency records, NS1) to direct clients to the nearest regional entry point. Within a region, a local load balancer distributes across SFU nodes. This minimizes RTT and keeps cross-region traffic to cascading links only.
Horizontal Scaling and Auto-Scaling
Scaling Signaling Servers
Because signaling is stateless (state lives in Redis), you can scale it like any web service. Add nodes behind the Layer 7 load balancer and use Horizontal Pod Autoscaler or cloud auto-scaling groups based on CPU or connection count.
Scaling Media Servers
Media nodes are stateful—you can’t just kill one mid-call. Instead:
- Pre-provision capacity with headroom (target 60–70% CPU).
- Drain gracefully — stop assigning new rooms, wait for existing ones to end.
- Scale out on demand using custom metrics (active streams, CPU).
sequenceDiagram
participant Mon as Monitoring
participant Auto as Autoscaler
participant New as New SFU Node
participant Coord as Coordinator
Mon->>Auto: CPU > 70% for 5 min
Auto->>New: Provision node
New->>Coord: Register + health check
Coord->>Coord: Add to pool
Coord-->>New: Ready for new rooms
Deployment and Configuration
Infrastructure Setup
A solid baseline:
- Compute: c5/c6i or equivalent, 4–8 vCPU per SFU node.
- Network: 1–10 Gbps, with UDP ports 10000–20000 open.
- OS tuning: raise
net.core.rmem_max,net.core.wmem_max, and UDP buffer sizes. - TLS: terminate signaling TLS at the load balancer; media uses DTLS-SRTP.
Configuration Examples
HAProxy for UDP media balancing:
frontend webrtc_media
bind *:10000-20000
mode tcp
default_backend sfu_nodes
backend sfu_nodes
mode tcp
balance leastconn
server sfu1 10.0.1.10:10000 check
server sfu2 10.0.1.11:10000 check
Mediasoup worker pool (Node.js):
const numWorkers = require('os').cpus().length;
const workers = [];
for (let i = 0; i < numWorkers; i++) {
const worker = await mediasoup.createWorker({
rtcMinPort: 10000 + i * 1000,
rtcMaxPort: 10999 + i * 1000,
});
workers.push(worker);
}
Docker Compose snippet for a signaling node:
services:
signaling:
image: myapp/signaling:latest
environment:
- REDIS_URL=redis://redis:6379
- SFU_POOL=sfu1:3000,sfu2:3000
ports:
- "443:443"
deploy:
replicas: 3
Monitoring, Maintenance, and Best Practices
Key Metrics to Monitor
| Metric | Why It Matters | Target |
|---|---|---|
| CPU per SFU | Headroom for spikes | < 70% |
| Outbound bandwidth | Capacity planning | < 80% NIC |
| Packet loss | Quality degradation | < 1% |
| Jitter | Audio/video glitches | < 30 ms |
| RTT | Perceived latency | < 150 ms |
| Active streams | Scaling trigger | Custom |
Use Prometheus + Grafana for dashboards and Alertmanager for thresholds. Mediasoup exposes stats via its API; Janus has an admin/monitor interface.
Operational Best Practices
- Graceful draining: never kill a busy SFU; drain it.
- Canary deploys: roll out new SFU versions to one node first.
- Chaos testing: simulate node failures to validate failover.
- Capacity buffer: keep 30% headroom for viral spikes.
- Document runbooks: on-call engineers need clear escalation steps.
Conclusion
Building a self-hosted WebRTC SFU cluster is a layered engineering problem: pick the right media server, split signaling from media, load balance UDP carefully, and scale stateful nodes with grace. Start with a single SFU, instrument everything, and cluster only when your metrics demand it. Whether you choose Janus for speed of development or Mediasoup for performance and control, the architectural patterns in this guide will carry you from a prototype to a production platform serving thousands of concurrent participants.
FAQ
What is the difference between an SFU and an MCU in WebRTC? An SFU forwards individual streams without mixing, reducing server CPU load. An MCU mixes all streams into a composite feed, lowering client bandwidth but raising server CPU usage. SFUs are preferred for scalable conferencing because they offload processing to clients and support flexible layouts.
How many participants can a single SFU handle? Typically 100–500 per server, depending on resolution, bitrate, and hardware. Mediasoup generally handles more participants per CPU core than Janus due to its efficient C++ core, but real limits depend on your use case and hardware.
What is the best way to load balance WebRTC media traffic? WebRTC media uses UDP, so traditional HTTP load balancers won’t work. Use UDP-capable balancers like HAProxy or Nginx with UDP support, and add geographic routing to send users to the nearest SFU.
Can I use Kubernetes to deploy a WebRTC SFU cluster? Yes, but with care. Expose UDP ports, use host networking or a UDP-capable CNI, and consider StatefulSets for media nodes. Autoscale using HPA with custom metrics like active stream count.
How do I handle cascading between SFUs in different regions? Connect SFUs across regions using a mesh or tree topology. Designate a primary SFU per conference, forward streams to secondaries, and coordinate via a central service. This reduces cross-region bandwidth at the cost of added latency.