Design Zoom: Building Real-Time Video at Scale Without Losing Your Mind
Design Zoom system design: architecture, WebRTC internals, SFU vs MCU, scaling to 1000+ participants, and production gotchas from real incidents..
20+ years shipping large-scale distributed systems. Everything here is grounded in real deployments.
- ✓Deep production experience
- ✓Understanding of internals and trade-offs
- ✓Experience debugging complex systems
Zoom uses a Selective Forwarding Unit (SFU) architecture where the server forwards selected video streams to each participant, reducing client processing. Key components: signaling server (WebSocket), media server (SFU), TURN server for NAT traversal, and a distributed backend for rooms and users.
Imagine a conference room where everyone talks at once. In a small room, you can hear everyone. But with 100 people, it's chaos. Zoom's SFU is like a smart switchboard operator: they listen to everyone, but only forward the voice of the person currently speaking to each listener. If you're not speaking, the operator stops sending your voice to others, saving everyone's ears (and bandwidth).
You've seen it happen: a 50-person all-hands call turns into a slideshow of frozen faces, audio stuttering like a scratched CD. Everyone blames the Wi-Fi. But the real culprit is almost always the server architecture. Most video calling systems choke because they try to send every participant's video to every other participant — an O(n²) problem that kills bandwidth and CPU. Zoom doesn't do that. And that's why it works when everything else falls apart.
This article breaks down the system design of a Zoom-like platform. You'll learn the exact architecture — signaling, media routing, scaling, and the production traps that take down naive implementations. By the end, you'll be able to design a real-time video system that handles 1000+ participants without melting your servers or your users' laptops.
Why SFU Beats MCU: The Bandwidth Math That Decides Your Architecture
Before you write a single line of code, you need to pick your media routing strategy. The two main options: MCU (Multipoint Control Unit) and SFU (Selective Forwarding Unit). MCU mixes all incoming streams into a single composite stream on the server. Each client sends one stream and receives one stream. Sounds simple. But the server has to decode, mix, and re-encode every stream — that's CPU-intensive and adds latency. SFU, on the other hand, forwards streams without decoding. The server is just a smart switch: it selects which streams to send to each client based on who's speaking. The client decodes multiple streams and renders them. This shifts the processing burden to clients, which is fine for desktops but tough for mobile. However, SFU scales linearly with participants (O(n) server load) while MCU scales O(n²) because the server must process every combination. For a 100-person call, MCU server does 100× the work of SFU. That's why Zoom uses SFU. The trade-off: SFU requires more client bandwidth (each client receives multiple streams), but you can mitigate with simulcast (send multiple resolutions) and bandwidth estimation.
Signaling: The WebSocket Dance That Sets Up Every Call
Before any video flows, clients need to exchange session descriptions and ICE candidates. This is signaling. You need a reliable, low-latency channel. WebSocket is the standard. Each client connects to a signaling server (typically a separate service from media servers). The signaling server handles room management, user presence, and relays SDP offers/answers and ICE candidates between clients. For a 1:1 call, signaling is simple: client A sends offer to server, server forwards to client B, B sends answer back. For group calls, the signaling server maintains a room state and broadcasts new participant info to all existing members. Key gotcha: signaling must be authenticated and rate-limited. If a malicious client floods the signaling server with SDP offers, it can exhaust server memory. Always validate SDP size (max 64KB) and limit offers per second per user (e.g., 5/s). Also, use a separate WebSocket connection for signaling vs. media — don't mix them. Media should go over UDP (SRTP/SCTP), not WebSocket.
Media Server Architecture: The SFU That Doesn't Drop Packets
The media server is the heart of your Zoom clone. It runs an SFU that receives RTP packets from publishers and forwards them to subscribers. Each media server handles a subset of participants (e.g., 100 per server). You need to assign participants to servers based on room size. For small rooms (<10), a single server is fine. For large rooms, you split participants across multiple servers and use a 'media bridge' to connect them. The bridge forwards streams between servers, effectively creating a distributed SFU. Each media server runs a WebRTC stack (e.g., mediasoup, Janus, or custom). Key components: a transport for each peer (WebRTC or plain RTP), a router that maps incoming streams to outgoing streams, and a bandwidth estimator that adjusts quality based on network conditions. The SFU must support simulcast: each publisher sends multiple resolutions (e.g., 720p, 360p, 180p). The SFU selects which layer to forward to each subscriber based on their bandwidth and screen size. This is critical for mobile clients on 3G. Without simulcast, you'd have to transcode, which kills latency.
maxIncomingBitrate per producer to avoid a single user flooding the server.Scaling to 1000+ Participants: Distributed SFU and Cascading
A single SFU can handle ~100-200 participants before CPU or bandwidth becomes a bottleneck. Beyond that, you need to distribute the load. Two approaches: 1) Room-based sharding: assign each room to a specific SFU. Works if rooms are small (<100). 2) Distributed SFU: split a single large room across multiple SFUs, each handling a subset of participants. The SFUs are connected via a media bridge (e.g., using RTP over UDP between servers). Each SFU forwards streams from its participants to other SFUs as needed. This is complex because you need to avoid forwarding the same stream multiple times. A common pattern is to designate one SFU as the 'bridge' for each stream, or use a full mesh between SFUs. For 1000 participants, you might have 10 SFUs, each handling 100 participants. Each SFU forwards the active speaker streams (3-6) to all other SFUs. That's 10 SFUs × 6 streams = 60 cross-SFU streams. Manageable. But you also need a global active speaker detection: the SFUs must agree on who's speaking. Use a centralized audio level aggregator that collects levels from all SFUs and broadcasts the top speakers.
Handling Network Degradation: Bandwidth Estimation and Adaptation
Real-time video is unforgiving of packet loss. WebRTC has built-in bandwidth estimation (GCC — Google Congestion Control) that adjusts bitrate based on delay and loss. But you need to configure it properly. The SFU should also participate: it can send REMB (Receiver Estimated Maximum Bitrate) messages to publishers to reduce their bitrate. For clients with poor connectivity, the SFU can switch to a lower simulcast layer or drop video entirely (audio-only). Key: never let the client decide alone — the server knows the overall network conditions. Implement a server-side bandwidth manager that aggregates feedback from all consumers and sends a unified REMB to each producer. Also, support FEC (Forward Error Correction) for audio — it's small and worth the overhead. For video, FEC is too expensive; use NACKs and retransmissions instead. And always enable packet loss hiding (PLC) in audio codecs (Opus does this automatically).
TURN Servers: The NAT Traversal Safety Net
Not all clients can establish peer-to-peer connections due to symmetric NATs or firewalls. That's where TURN (Traversal Using Relays around NAT) comes in. TURN servers relay media traffic. They're bandwidth hogs: each stream consumes relay bandwidth. You need to deploy TURN servers in multiple regions close to users. Use ICE (Interactive Connectivity Establishment) to try direct P2P first (via STUN), then fall back to TURN. Configure TURN with authentication (time-limited credentials) to prevent abuse. Key metric: TURN usage ratio. If >20% of calls use TURN, your STUN infrastructure might be misconfigured or your users are behind restrictive NATs (e.g., corporate VPNs). Also, TURN servers must support UDP, TCP, and TLS. UDP is preferred for low latency. TCP adds overhead but works through firewalls that block UDP.
Recording and Playback: Archiving the Chaos
Recording a Zoom call is harder than it looks. You can't just record the mixed audio/video because you lose individual speaker tracks. For compliance (e.g., legal depositions), you need per-participant recordings. Solution: have the SFU send each participant's stream to a recording service. The recording service can either store individual tracks (for later compositing) or mix them in real-time. For real-time mixing, use a dedicated MCU-like component that decodes and mixes streams, then encodes the final video. This is expensive. Better: store individual tracks as fragmented MP4 (fMP4) with timestamps, and composite offline. For playback, you need a video player that can handle multiple synchronized streams. Use HLS or DASH with multiple audio tracks. Or build a custom player using WebRTC to re-render the call. Gotcha: recording must handle network glitches — buffer at least 5 seconds of data to recover from packet loss.
When Not to Build Your Own Zoom: The Build vs. Buy Decision
Building a Zoom clone is a massive undertaking. You need expertise in WebRTC, networking, distributed systems, and media codecs. If your core business isn't video conferencing, don't build it. Use a third-party API like Twilio Video, Agora, or Daily.co. They handle SFU, TURN, and scaling. You pay per participant-minute, but you save months of engineering. Only build if you have specific requirements: custom UI, offline recording, proprietary codecs, or air-gapped deployments. Even then, consider using open-source SFUs like mediasoup or Janus and customize. The build vs. buy decision is simple: if you need to support >1000 participants with <200ms latency, and you have a team of 5+ engineers dedicated to this, build. Otherwise, buy.
The 4GB Container That Kept Dying
- Never forward all streams.
- Always limit active video streams to a small number (3-6).
- Use audio levels to select which streams to forward.
ss -s or WebRTC stats). 2. Check bandwidth estimation logs. 3. If loss >5%, enable FEC for audio or switch to lower simulcast layer. 4. Verify TURN server bandwidth isn't saturated.kubectl top pod <pod-name> --containerskubectl logs <pod-name> --previous | grep OOM--max-incoming-bitrate 2000000 per producer and limit active video streams to 4.| File | Command / Code | Purpose |
|---|---|---|
| SignalingFlow.systemdesign | onMessage(ws, msg) { | Signaling |
| SFUInternal.systemdesign | class SFU { | Media Server Architecture |
| DistributedSFU.systemdesign | class Bridge { | Scaling to 1000+ Participants |
| BandwidthAdaptation.systemdesign | class BandwidthManager { | Handling Network Degradation |
| TURNDeployment.systemdesign | listening-port=3478 | TURN Servers |
| RecordingService.systemdesign | class RecordingService { | Recording and Playback |
Key takeaways
Interview Questions on This Topic
How does Zoom's SFU handle a participant with poor network connectivity? Describe the adaptation mechanism.
Frequently Asked Questions
20+ years shipping large-scale distributed systems. Everything here is grounded in real deployments.
That's Real World. Mark it forged?
5 min read · try the examples if you haven't