A high-performance SFU (Selective Forwarding Unit) optimized for HD meetings and large-scale deployment.
The SFU core is built around a lock-free room execution model. Each room is processed by a dedicated isolated thread, ensuring deterministic packet routing without shared-state contention. Media packets are forwarded through a zero-copy pipeline powered by AF_XDP (io_uring fallback), enabling efficient fan-out to thousands of subscribers with minimal CPU overhead.
AF_XDP is the default network backend; io_uring is available as a fallback for environments where AF_XDP isn't supported.
Try a live meeting at mezon.ai/meet. You must log in to create a meeting.
Existing SFUs options are capable, but heavy — large dependency trees, runtime overhead, and general-purpose designs that aren't tuned for any one use case. mezon-sfu exists because we needed something different:
- Standalone, dependency-light: No external media-server runtime to operate — build it, run the binary.
- Deeply optimized for meetings: Built from scratch around the meeting workload specifically (HD video, screen share, large rooms) rather than adapted from a general-purpose media server.
- Lightweight and easy to integrate: Small enough to embed into the existing Mezon ecosystem without dragging in a heavyweight stack.
- WebRTC Compliance: Compatible with both standard WebRTC clients and libmezia — a lightweight, ultra-low-latency audio/video library for native platforms
- High-Performance Routing: Low-latency packet pool design paired with multi-threaded worker pipelines
-
Epoch-Based Active Pacing: Generation-stamped
$O(1)$ active session deduplication and frame-boundary gating (ready_count > 0), eliminating periodic$O(N)$ scans across idle sessions - Contiguous RTX Slab Cache: Single-allocation contiguous memory slabs per stream cache, eliminating 1,024 dynamic heap allocations during track setup
- Zero-Copy Ingress Fast Path: Ciphertext buffer preservation lazily gated to packets requiring ROC recovery or key rollover fallback
- Static Metric Dispatch: Enum and X-macro ID-based atomic counters eliminating runtime string hashing on media forwarding paths
- Native Security: Integrated DTLS handshake and secure SRTP packet protection
- WebRTC Test Client: Includes a diagnostic HTML WebRTC client to verify connectivity
- Simple, Standalone Setup: No external dependencies required to get running
- Lock-free fanout with hazard pointers
- Full SVC temporal/spatial layer support
- Modern zero-copy network stack with AF_XDP backend and io_uring fallback
- Standards-compliant GCC congestion control
- Push To Talk (PTT): Native support
Before building, ensure you have the following installed:
- CMake (3.15 or higher)
- C Compiler (GCC or Clang)
- BoringSSL & libsrtp2 development libraries
Run these steps in order from the repository root.
sudo apt install libuv1-devgit clone https://github.com/microsoft/mimalloc.git
cd mimalloc
mkdir -p build && cd build
cmake ..
make -j$(nproc)
sudo make installgit clone https://boringssl.googlesource.com/boringssl
cd boringssl
mkdir build && cd build
cmake -DCMAKE_BUILD_TYPE=Release ..
make -j$(nproc)
sudo make install
sudo ldconfigcd boringssl
sudo mkdir /usr/local/include/boringssl
sudo cp -rf include/* /usr/local/include/boringssl/
sudo cp -rf build/bssl /usr/local/bin/
sudo mkdir /usr/local/lib/boringssl
sudo cp -rf build/lib* /usr/local/lib/boringssl/git clone https://github.com/cisco/libsrtp.git
cd libsrtp
git checkout 24b3bf8
./configure --enable-openssl \
crypto_CFLAGS="-I/usr/local/include/boringssl/" \
crypto_LIBS="-L/usr/local/lib/boringssl/ -lcrypto -lstdc++"
makegit clone https://github.com/nats-io/nats.c.git
cd nats.c
mkdir build && cd build
cmake .. -DNATS_BUILD_STREAMING=OFF -DNATS_BUILD_EXAMPLES=OFF
make -j$(nproc)
sudo make install
sudo ldconfigPick one of the two options below.
Install clang, libxdp, libbpf, and matching Linux headers:
sudo apt install clang libxdp-dev libbpf-dev linux-headers-$(uname -r)To build libxdp-dev from source instead:
sudo apt install libpcap-dev
git clone --recurse-submodules https://github.com/xdp-project/xdp-tools.git
cd xdp-tools
# Fetch all tags and update submodules
git fetch --tags
git submodule update --init --recursive
# Find the latest release tag
git tag -l "v*" | tail -n 5
# Checkout the latest stable release (e.g., v1.4.2)
git checkout v1.4.2
# Ensure submodules match the selected release tag
git submodule update --init --recursive
# Clean previous build artifacts
make clean
# Run configure script to generate build configuration
./configure
# Build and install
make
sudo make install
# libbpf first (libxdp depends on it)
sudo make -C lib/libbpf/src install PREFIX=/usr/local LIBDIR=/usr/local/lib
# then libxdp
sudo make -C lib/libxdp install PREFIX=/usr/local LIBDIR=/usr/local/libThen configure and build mezon-sfu:
cmake -S . -B build
cmake --build build -j$(nproc)Install liburing and disable AF_XDP explicitly:
git clone https://github.com/axboe/liburing.git
cd liburing
./configure
make -j$(nproc)
sudo make install
cd -
cmake -S . -B build-uring -DSFU_NET_BACKEND="io_uring"
cmake --build build-uring -j$(nproc)mkdir build && cd build
cmake .. -DCMAKE_PREFIX_PATH=/usr/local -DCMAKE_BUILD_TYPE=Release
make -j$(nproc)
ctest --output-on-failureThe compiled binary will be generated at ./build/mezon-sfu.
| CMake Flag | Default | Description |
|---|---|---|
-DCMAKE_BUILD_TYPE |
Release |
Build mode (Release, RelWithDebInfo, or Debug). |
-DSFU_NET_BACKEND |
af_xdp |
Media network stack: af_xdp (high-performance zero-copy) or io_uring (container-friendly fallback). |
-DSFU_DIAG_LOG |
OFF |
Diagnostic logging for SRTP key-desync and packet tracing (OFF = zero-overhead silent mode, ON = verbose drops/dumps). |
-DSFU_ENABLE_ASAN |
OFF |
Compile with AddressSanitizer for memory bug detection. |
-DSFU_ENABLE_TSAN |
OFF |
Compile with ThreadSanitizer for race condition detection. |
-DSFU_ENABLE_PRIVILEGED_TESTS |
OFF |
Register network namespace integration tests requiring root privileges. |
The production container uses the io_uring backend. It does not require host networking, eBPF access, or a privileged container. The host must provide a modern Linux kernel and adequate nofile and memlock limits.
Create the runtime environment file and replace both placeholders:
cp .env.example .env
# SFU_PUBLIC_HOST must be the public IP or DNS name clients can reach.
# SFU_JWT_SECRET must be a long random secret and must match the token issuer.Build and start NATS and the SFU:
docker compose build --pull
docker compose up -d
docker compose ps
docker compose logs -f sfuThe container exposes UDP 7000 for WebRTC media and TCP 8000 for signaling by default. Open both ports in the host and cloud firewalls. Docker/Kubernetes secrets can be mounted and selected with SFU_JWT_SECRET_FILE; this takes precedence over SFU_JWT_SECRET.
The health check performs a WebSocket upgrade against the signaling listener. Stop gracefully with:
docker compose down --timeout 30AF_XDP remains available for native-host deployments. It is intentionally not used by this container because it requires host networking, the physical NIC and queue configuration, BPF access, and elevated capabilities.
tools/loadtest is a separate Go/Pion client that exercises JWT authentication, WebSocket signaling, ICE, DTLS-SRTP, renegotiation, synthetic Opus/VP8 publishing, and subscriber RTP reception.
Requirements: Go 1.24+, an accessible signaling URL, open UDP media routing, and the same JWT secret configured on the SFU.
cd tools/loadtest
go test ./...
go run . \
-url ws://127.0.0.1:8000/ws \
-jwt-secret 'replace-with-the-sfu-jwt-secret' \
-rooms 1 \
-peers 3 \
-speakers 1 \
-duration 30s \
-ramp-duration 5s \
-bitrate 240000 \
-json-file ../../loadtest-results/smoke-report.jsonFor the production topology, run:
cd tools/loadtest
mkdir -p ../../loadtest-results
go run . \
-url ws://SFU_HOST:8000/ws \
-jwt-secret 'replace-with-the-sfu-jwt-secret' \
-rooms 30 \
-peers 10 \
-speakers 2 \
-duration 60m \
-ramp-duration 30s \
-bitrate 240000 \
-min-success-rate 100 \
-max-packet-loss 1 \
-min-rx-packets 1 \
-json-file ../../loadtest-results/capacity-report.jsonThe process exits nonzero when an enabled threshold fails. Use go run . -help for all topology, media, threshold, and reporting options.
Copy the environment example, set a strong JWT secret, and create the report directory:
cp .env.example .env
# Edit .env: set SFU_PUBLIC_HOST and SFU_JWT_SECRET.
mkdir -p loadtest-resultsRun a small functional smoke test locally:
mkdir -p loadtest-results
LOADTEST_ROOMS=1 \
LOADTEST_PEERS_PER_ROOM=3 \
LOADTEST_SPEAKERS_PER_ROOM=1 \
LOADTEST_DURATION=30s \
docker compose -f compose.yaml -f compose.loadtest.yaml up \
--build --abort-on-container-exit --exit-code-from loadtestRun the target topology of 300 participants across 30 rooms, with two speakers per room:
mkdir -p loadtest-results
docker compose -f compose.yaml -f compose.loadtest.yaml up \
--build --abort-on-container-exit --exit-code-from loadtestThe report is written to loadtest-results/report.json. Defaults require all peers to connect, subscriber media reception, and no more than 1% measured packet loss. Override LOADTEST_DURATION, LOADTEST_RAMP_DURATION, LOADTEST_VIDEO_BITRATE, LOADTEST_MIN_SUCCESS_RATE, LOADTEST_MAX_PACKET_LOSS, and LOADTEST_MIN_RX_PACKETS when needed.
Stop and remove the test stack after completion:
docker compose -f compose.yaml -f compose.loadtest.yaml down --timeout 30A same-host Compose run is a functional regression test, not authoritative capacity evidence. For production qualification, run the load generator from separate machines for at least 60 minutes and record SFU CPU, RSS, NIC throughput/drops, container restarts, and packet-pool or queue exhaustion logs. Approve 300-user capacity only with at least 30% CPU and network headroom.
mezon-sfu can be configured using environment variables and runtime flags.
Config file: config.ini is the default configuration used for local runs.
Edit publish_host in config.ini:
- Set it to
127.0.0.1for local testing. - Set it to your server's external public IP (e.g.,
203.0.113.88) when deploying to a remote host.
Best for local development or single-server environments.
./build/mezon-sfu -c ../config.iniModern WebRTC engines restrict local network discovery (mDNS protection) when running files directly off the hard drive. You must host the test client over HTTP/HTTPS to test it successfully.
- Open your browser and navigate to
http://localhost:3000/webrtc_test_client.html. - Input your WebSocket signaling URL (e.g.,
ws://127.0.0.1:8080). - Input
jwt_secret(fromconfig.ini). This is for testing purposes only. - Click Connect to start streaming!
See examples/webrtc_test_client.html and its README for the full client — camera + screen-share publishing, speaker/audience join modes, and a field-by-field walkthrough of the join form.
mezon-call-translation is a companion project that joins calls on this SFU to record them, transcribe speech in real time, and optionally speak synthesized audio back into the call. It talks to mezon-sfu's own WebSocket/WebRTC signaling directly.
What it does:
agents/cmd/agent(Go,pion/webrtc) joins a room as a regular WebRTC participant over mezon-sfu's JWT-authenticated signaling — same track/midlayout as any other client, no special-cased negotiation.- It forwards each mic track's decoded PCM to a
record-servicefor durable recording and, per track, to a realtime speech-to-text service — the STT engine is Nemotron (NVIDIA's cache-aware streaming ASR model), not Vosk. - In
speakermode, it synthesizes speech with Kokoro-82M TTS, Opus-encodes it, and publishes it back into the room as its own outgoing track — e.g. for speaking translated audio into a call. - A separate long-lived process,
agents-bot, logs into Mezon itself to resolve participant identities and bridge chat, since mezon-sfu has no data channel or identity API of its own.
How it connects:
- Agent lifecycle is driven by a NATS start/stop event published by BE mezon, not a REST call into the SFU or the agent:
agents/cmd/worker-managersubscribes to that subject and spawns/kills oneagentsubprocess per active room. - The agent dials mezon-sfu's signaling WebSocket directly (
SFU_WS_URL) with an HS256-signed JWT for the WebRTC session itself; audio/transcript exchange with the STT backend happens over a separate WebSocket (/ws/transcription/) to thestt_serviceprocess, not to mezon-sfu. stt_servicehealth is exposed via/healthand/health/simple; each connected track gets its own dedicated STT pipeline, up to a configured concurrency limit.
Setup: see agents/README.md for build/run instructions and full environment configuration, and agents-bot/README.md for the companion bot process.
If you use the Zed editor, you can run and debug your builds directly with CodeLLDB by configuring your workspace tasks:
[
{
"label": "Debug mezon-sfu (CMake Both)",
"adapter": "CodeLLDB",
"build": {
"command": "cmake",
"args": ["--build", "build"],
"cwd": "$ZED_WORKTREE_ROOT"
},
"program": "$ZED_WORKTREE_ROOT/build/mezon-sfu",
"args": ["-c", "config.ini"],
"request": "launch"
}
]The dispatcher owns UDP receive completions, packet-to-worker hashing, and worker-inbox delivery. STUN, DTLS, SRTP, RTP/SVC parsing, congestion control, scheduling, routing, protection, and send-ring processing execute on pinned worker threads.
The media path follows a streamlined pipeline:
sfu_dispatch_packet() → sfu_ingress_process() → sfu_router_forward() → sfu_egress_process()
Fanout crosses the worker-to-worker SPSC mesh only when a subscriber is owned by another worker. Publisher-uplink TWCC is generated by the SFU from received publisher RTP, while subscriber-downlink TWCC is parsed and fed into GCC for pacing and layer selection. Ordinary forwarded RTP remaps the payload type and transport-wide sequence extension without modifying source timestamps or sequence numbers.
-
Generation-Based Active Pacing (
$O(1)$ Membership & Drain): Instead of scanning all registered sessions every 2 ms ($O(N)$ linear scans), worker pacing runs only across sessions currently holding pending work (paced_active_sessions).-
Frame boundary gating: Pacing activation is gated on complete video frames (
ready_count > 0withsource_marker), preventing spurious worker wakeups on partial multi-packet frames still being assembled. -
Epoch generation stamping: Membership deduplication is an
$O(1)$ check comparings->paced_generationtow->paced_generation. -
Cross-worker migration safety: The high 16 bits of
w->paced_generationencode(worker_index + 1), ensuring generation stamps from a previous worker never collide when sessions migrate across threads. -
Scratch-buffered drain: On each 2 ms drain tick, active sessions are batched into a scratch buffer,
w->paced_generationis advanced, and sessions with residual work are requeued under the new epoch without reallocating arrays.
-
Frame boundary gating: Pacing activation is gated on complete video frames (
-
Contiguous RTX Slab Cache: The 1,024-packet RTX retransmission cache preallocates its entire payload storage in a single contiguous memory slab per video runtime. This eliminates thousands of dynamic heap allocations and deallocations during subscriber stream setup, removing heap fragmentation and track initialization latency.
-
Zero-Copy Ingress Fast Path: Ingress SRTP processing operates zero-copy on the fast path. Defensive ciphertext preservation is lazily restricted to packets that can legitimately enter rollover counter (ROC) recovery or key rollover fallback.
-
Static Metric Dispatch: Metric counters use compile-time X-macro enumerations (
sfu_metric_id_t). Media forwarding paths update counters through direct index lookups (sfu_metric_id_inc), eliminating string hashing and table searches from high-frequency packet routines. -
Open-Addressed Room Registry: Room lookups use open-addressed hash tables with explicit slot recycling and generational retirement, replacing linear searches and preventing unbounded table growth during continuous room creation and teardown.
Configure the interface and hardware queue in config.ini:
[af_xdp]
interface = eth0
queues = auto
frame_count = 16384
frame_size = 4096
mode = native # native, skb, or autoThe AF_XDP binary requires permission to load BPF programs and administer the selected interface (normally root or appropriate CAP_BPF/CAP_NET_ADMIN capabilities). It supports IPv4 UDP and does not reassemble fragments. The configured frame pool is split evenly into power-of-two RX and TX rings. Configure RSS/flow steering so the media UDP port reaches the selected queue; matching media packets on another queue are dropped rather than passed to an undrained UDP socket. On a cold neighbour entry, TX temporarily falls back to the bound nonblocking UDP socket so the kernel can resolve ARP, then direct AF_XDP transmission resumes. The loader refuses to replace an existing XDP program, and cleanup detaches only the program attached by this process.
The AF_XDP frame and software-ring unit tests are CPU-only and do not require root or a network interface:
ctest --test-dir build --output-on-failure -R 'af_xdp_(frame|ring)'A privileged veth/network-namespace smoke test is available but is not registered by default:
cmake -S . -B build-af-xdp-integration -DSFU_ENABLE_PRIVILEGED_TESTS=ON
cmake --build build-af-xdp-integration -j$(nproc)
sudo ctest --test-dir build-af-xdp-integration --output-on-failure -R af_xdp_veth_integrationThe script uses a temporary single-queue veth pair in skb mode and cleans up its namespace, links, process, and XDP attachment on exit.
Results below are smoke-run figures from a single development machine, not guaranteed performance targets. For comparative measurements, use a non-quick run with CPU affinity and frequency scaling controlled.
Run the focused frame parser/builder benchmark with:
build-af-xdp/benchmark/bench_af_xdp_frame all --quick
build-af-xdp/benchmark/bench_af_xdp_frame parse_ipv4_udp --packet-size 1200
build-af-xdp/benchmark/bench_af_xdp_frame build_ipv4_udp --packet-size 1200Example results for a 1200-byte payload (--quick, 1,000 measured iterations):
| Benchmark | Time per operation | Operations per second |
|---|---|---|
| IPv4/UDP frame parsing | 39.39 ns | 25.39 M |
| VLAN IPv4/UDP frame parsing | 39.12 ns | 25.56 M |
| IPv4/UDP frame construction | 179.17 ns | 5.58 M |
| IPv4 header checksum | 34.90 ns | 28.65 M |
Run from a clean release build with ./build/benchmark/bench_sfu_core (1,000,000 iterations, 1,200-byte synthetic RTP packets, 4 workers, fan-out 3). Machine: i5-10400, 12 logical cores.
| Benchmark | What it measures | Wall time | Throughput |
|---|---|---|---|
rtp_parse |
RTP parsing | ~25 ns/packet | ~39.5 M packets/s |
packet_pool |
Packet-pool alloc/retain/release cycle (media-path prerequisite) | ~196 – 260 ns/op | ~3.8 – 5.1 M ops/s |
fanout_mesh |
SPSC worker-mesh enqueue + drain (job = 1 packet × 3 targets) | ~45 ns/job | ~22.4 M jobs/s |
media_fanout |
End-to-end fanout | ~243 – 256 ns/target | ~3.9 – 4.1 M targets/s |
srtp_decrypt |
SRTP unprotect (AES-128-GCM) on a 1200-byte packet | ~379 ns/packet | ~2.64 M packets/s |
srtp_encrypt |
SRTP protect (AES-128-GCM) on a 1200-byte packet | ~376 ns/packet | ~2.66 M packets/s |
SRTP is measured on a single stream with monotonically increasing transport sequence (so replay/ROC state advances like real media). The remaining pipeline stages (UDP ingress, STUN, DTLS, SVC parse, congestion control, layer scheduler) still have no dedicated harness.