Design Zoom
Real-time audio, video and screen share for 2 to 1,000 people per meeting, 300 M participants a day, under 150 ms mouth to ear.
Last updated 2026-10-26. Difficulty: hard. Patterns: webrtc, sfu, real-time-media, congestion-control, multi-region. Reported at Zoom, Google, Microsoft, Meta, Amazon, Discord.
The interviewer asks, the candidate answers and draws, and you press Next. Skip to any stage or key moment, and ask the AI Mentor anything along the way.
Or sit it in the style of Google · Microsoft · Meta · Amazon, run as that company’s round is reported to go.
The short answer
How to design Zoom in a system design interview, in five steps:
- Scope it. Scheduled and ad-hoc meetings, join from a browser or an app, audio and video for 2 to 1,000 participants, screen share, in-meeting chat, host controls, cloud recording.
- Set the targets. Mouth-to-ear latency: < 150 ms in region; Scale: 300 M participants/day · 20 M concurrent; Quality adapts per receiver: audio before video; Join time: p90 < 2 s to first audio.
- Do the numbers. Concurrent participants at peak: ~10 M avg · ~20 M peak; Concurrent meetings at peak: ~4 M; Join rate at the top of the hour: ~3.5 k/s avg · ~40 k/s spike.
- Draw the design. Edge gateway, Meeting service, Meetings DB, Live meeting state, SFU allocator, Event bus, STUN / TURN relay, Edge SFU, Recording bot.
- Go deep where it is hard.
- Mesh vs MCU vs SFU → SFU: receive once, forward packets without decoding.
- What each receiver gets → Last-N by voice activity, simulcast layer per receiver from layout and estimated bandwidth.
- UDP, packet loss and firewalls → RTP over UDP with DTLS-SRTP, adaptive jitter buffer, NACK, FEC and concealment.
- Placing meetings and cascading SFUs → Nearest-region SFU per participant, cascaded over the backbone.
- An SFU dies mid-meeting → Detect, reassign, and have clients ICE-restart to a new SFU over signaling.
- Cloud recording without slowing the call → Bot participant writes raw per-track chunks; composite and transcode after the meeting.
Functional requirements
- Scheduled and ad-hoc meetings. A meeting id and link exist ahead of time (one-off or recurring), or are created on the spot. The live session exists only while someone is in it; most meetings are 2 to 5 people.
- Join from a browser or an app. Browsers use standard WebRTC with nothing to install; native apps speak the same protocols with hardware codecs and their own tuning. Join by link, with an optional passcode or waiting room.
- Audio and video for 2 to 1,000 participants. Anyone can unmute. Speaker view (one large tile) and gallery view (up to 25 tiles). A receiver on a weak link gets lower quality without affecting anyone else.
- Screen share. Text must stay sharp: high resolution at a low frame rate (5 to 15 fps), and it outranks camera video when bandwidth is short.
- In-meeting chat. To everyone or to one person, in one order for all participants, with recent history shown to late joiners and saved with the meeting.
- Host controls. Mute, remove, lock, admit from the waiting room, spotlight a speaker. Several people change this shared state at once, so it needs a single order.
- Cloud recording. Speaker view with the screen share, plus audio-only, ready within minutes of the meeting ending. Everyone in the meeting can see that it is being recorded.
- Out of scope. Phone dial-in and SIP room systems (an audio-mixing gateway that joins as a participant), webinars beyond 1,000 attendees (switch to live streaming over HLS), end-to-end encryption, live captions and AI summaries, billing.
Non-functional requirements
- Mouth-to-ear latency (< 150 ms in region). ITU-T G.114 puts comfortable conversation under about 150 ms one way. Capture, encode, jitter buffer and decode use ~80 ms, leaving ~70 ms for the network and the server. This is the requirement that drives the hardest trade-off: no buffering and no CDN, UDP instead of TCP, losses concealed rather than awaited, and a media server within ~20 ms of most users. Across continents physics sets the floor, ~235 ms New York to Singapore.
- Scale (300 M participants/day · 20 M concurrent). About 4 M live meetings at peak and ~40 k joins a second at the top of the hour. Most meetings are tiny; a few have 1,000 people.
- Quality adapts per receiver (audio before video). One person on a weak link must never degrade anyone else, and when bandwidth runs out, video degrades first. Audio should stay intelligible at around 10 % random packet loss.
- Join time (p90 < 2 s to first audio). REST join, WebSocket, SDP exchange, ICE and DTLS each cost a round trip or two, so servers must be close to users and the join path must survive the top-of-hour spike.
- Connectivity (works with only TCP 443 open). Some corporate, hotel and school networks block UDP or force an HTTPS proxy. Joins must still succeed there, at somewhat worse quality.
- Resilience (media back < 5 s after an SFU fails). A media server crash costs seconds of frozen media, never the meeting: roster, chat and host state survive, and clients reconnect on their own.
- Security and privacy (encrypted in transit · admitted users only). DTLS-SRTP on every media hop and TLS for signaling; only participants the meeting service admitted can connect to its SFU. Recordings are encrypted at rest, visible only to the account, and deleted on its retention schedule.
Back-of-envelope estimates
- Concurrent participants at peak: ~10 M avg · ~20 M peak. 300 M participant sessions a day × 48 minutes ÷ 1,440 minutes ≈ 10 M in meetings at any moment. Business hours in the Americas and Europe overlap, so the peak is about 2×: 20 M.
- Concurrent meetings at peak: ~4 M. 20 M participants ÷ ~5 per meeting = 4 M live meetings. The meeting, not the participant, is the unit the allocator places, so this is the number it juggles.
- Join rate at the top of the hour: ~3.5 k/s avg · ~40 k/s spike. 300 M ÷ 86,400 ≈ 3.5 k/s on average. But meetings start on the hour: if the peak hour has 25 M joins and 30 % land in the 3 minutes around :00, that is 7.5 M ÷ 180 s ≈ 42 k/s. The join API, the allocator and the SDP exchange are sized for the spike, not the average.
- One sender’s uplink: ~2.2 Mbps · ~275 packets/s. Simulcast 180p 150 kbps + 360p 500 kbps + 720p 1.5 Mbps + Opus audio 32 kbps ≈ 2.2 Mbps. Video in ~1,200-byte packets is ~225 packets/s, plus 50 audio packets/s (20 ms frames). That is ~45 % more than 720p alone, which is why the SFU pauses layers nobody is watching.
- One receiver’s downlink: ~2.8 Mbps (vs ~150 Mbps unbounded). One large tile at 720p 1.5 Mbps + 8 thumbnails at 180p 8 × 150 kbps = 1.2 Mbps + top-3 audio ~0.1 Mbps ≈ 2.8 Mbps, whatever the meeting size. Forwarding all 999 others as thumbnails in a 1,000-person meeting would be ~150 Mbps per receiver and ~150 Gbps for the meeting. Selection is the design, not an optimisation.
- Peak SFU egress: ~40 Tbps. Many participants are audio-only, camera-off or in two-person meetings, so the average downlink is ~2 Mbps: 20 M × 2 Mbps = 40 Tbps. Spread over ~20 regions, that is ~2 Tbps per region.
- SFU servers: ~4,000 at full load · ~6,000 with headroom. A forwarding server does ~10 Gbps of egress, about 1 M packets/s at 1,200 bytes, each packet encrypted for its own receiver. 40 Tbps ÷ 10 Gbps = 4,000 servers; run at 70 % to absorb failures, ~6,000. That is ~3,500 participants on a typical server, and a crash moves all of them at once.
- Recording storage: ~500 TB/day. If 1 M meeting-hours a day are recorded and the composite runs at ~1.1 Mbps (≈ 0.5 GB per hour), that is 500 TB/day, ~180 PB a year before retention. Raw per-track chunks are 2 to 3× larger but are deleted a week after the composite succeeds.
Components
- Meeting client (browser (WebRTC) · desktop · mobile): Captures and encodes one audio stream and three simulcast video layers, and decodes the handful of streams the SFU forwards to it. Keeps a signaling WebSocket and a separate media connection, so either can fail without the other. Browsers use the standard WebRTC stack; native apps can use hardware encoders and tune congestion control and reconnects themselves.
- Edge gateway (REST + WebSocket · TLS 443): Terminates TLS for the REST API (schedule, join, recordings) and holds each participant’s signaling WebSocket. Routes in-meeting messages to the instance that owns the meeting and fans events from it back out to the participants it holds. Never carries media, so a gateway restart costs a reconnect, not a dropped call.
- Meeting service (one owner per live meeting): The control plane of a live meeting: admission and waiting room, roster, roles, mutes and host actions, chat order, recording state, and which SFUs carry the meeting. Each live meeting is owned by one instance through a lease with an epoch, so every change is serialised and a stale owner is fenced. It tells SFUs who may connect and what they publish; it never touches a media packet.
- Meetings DB (Postgres · sharded by meeting id): Durable meeting records: schedule and recurrence, settings (passcode, waiting room, allowed regions), host and account, plus chat logs and recording metadata. Read at join and cached per meeting, written rarely. The live session’s fast-changing state deliberately does not live here.
- Live meeting state (Redis · replicated): The hot state of every live meeting: owner lease and epoch, roster, current speaker, the last 100 chat messages, and the meeting-to-SFU map per region. Written on every change so a new owner can take over in seconds. A few kilobytes per meeting across ~4 M meetings at peak, so tens of gigabytes in total.
- SFU allocator (load- and RTT-aware): Knows every SFU’s health and load from one-second heartbeats and picks servers for meetings: the participant’s lowest-RTT region, then the server with the most headroom, never past ~70 % so a neighbour’s failure can be absorbed. Spreads large meetings across several servers and finds replacement capacity when a server dies. Its view is soft state, rebuilt from heartbeats within seconds of a restart.
- Event bus (Kafka · key = meeting id): Meeting lifecycle events (started, ended, recording stopped, media recovered) and quality telemetry, keyed by meeting id so one meeting’s events stay in order. Feeds the composite pipeline, usage reporting and quality dashboards. Nothing on the live media path ever waits on it.
- STUN / TURN relay (UDP · TURN over TLS 443): Fallback connectivity for networks that block UDP or only allow HTTPS through a proxy: relays the participant’s media to the SFU over TLS on port 443. Colocated with SFUs, so the extra hop is about a millisecond. Credentials are short-lived HMACs issued at join and relaying is restricted to SFU addresses, so it is not an open proxy; STUN on the same servers serves peer-to-peer calls.
- Edge SFU (selective forwarding · UDP): Receives each participant’s media once and forwards packets to subscribers without decoding, choosing per receiver which streams (last-N by voice activity) and which simulcast layer fit its bandwidth estimate. Terminates DTLS-SRTP per participant, answers NACKs from a one-second packet cache and rate-limits keyframe requests. About 10 Gbps and 1 M packets a second per server, run to ~70 %.
- Recording bot (receive-only participant): A headless client launched from a warm regional pool when a host starts recording. Joins the SFU like any participant, subscribes to every audio track, the active speaker at 720p and the screen share, and writes the frames to object storage in 10-second chunks without decoding. It appears in the roster, so everyone can see the meeting is being recorded.
- Recording storage (S3 / GCS · encrypted): Raw per-track chunks with a manifest, and the finished outputs (MP4, audio-only, HLS) per account. Raw tracks are deleted a week after a successful composite; finished recordings follow the account’s retention policy. About 500 TB of new finished video a day at 1 M recorded hours.
- Remote-region SFU (same meeting, other region): An SFU in another region serving that region’s participants of the same meeting. Exchanges streams with the meeting’s other SFUs over the private backbone, subscribing once per stream on behalf of all its local receivers, and shares audio levels so every region agrees on who is speaking.
- Composite + transcode (batch · parallel by segment): Consumes recording.stopped, decodes and aligns the raw tracks by wall-clock time, renders speaker view with the share, and encodes MP4, audio-only and HLS renditions. Splits long recordings into 5-minute pieces encoded in parallel, so an hour is ready in minutes. Idempotent on recording id, so a redelivered event never produces a second file.
- Playback CDN (recordings only): Serves recorded meetings as HLS with signed, expiring URLs, pulling from recording storage on a miss. Live meetings never touch it: real-time media has no time for a cache. Deleting a recording purges it here too.
User flows
- Join a meeting and start talking. Signaling and media take different paths: REST and a WebSocket set the meeting up, then audio and video go over UDP straight to an SFU in the participant’s nearest region. A strong answer knows what each round trip on the way to first audio is for.
- The client asks to join with the meeting id and passcode, and gets a short-lived join token. The meeting service reads the meeting’s settings (passcode, waiting room, allowed regions, host) from the Meetings DB, cached per meeting. The token is a signed JWT valid for 60 seconds and bound to (meeting, user), so it cannot be replayed into another meeting. The response also carries TURN credentials, so the fallback costs no extra round trip if UDP turns out to be blocked.
- The client opens a WebSocket and sends join; the instance that owns this meeting admits it. Each live meeting has exactly one owning meeting-service instance, recorded as a lease in the live state store, and the gateway routes every message for that meeting there. One owner serialises joins, mutes and host actions, so two co-hosts clicking "mute all" and "unmute" never apply in different orders on different clients. With a waiting room, the participant waits here until a host admits them.
- The meeting service finds the meeting’s SFU in the participant’s region, asking the allocator to place one if there is none. The region is the lowest-RTT one from the client’s probes, not GeoIP, which is often wrong for VPN, corporate and mobile users. The allocator picks the server with the most headroom from the heartbeats it receives every second and stops placing on a server at ~70 % of its egress or packet budget. The assignment is written to the live state store under the meeting’s epoch.
- The client sends an SDP offer with its audio and three video layers; the SFU answers through the meeting service. The offer lists Opus for audio and one video track with three simulcast encodings (rids q, h, f), plus the header extensions the SFU routes on: audio level, transport-wide sequence numbers and layer ids. The SFU is ICE-lite: its answer carries only its own public UDP and TCP 443 candidates and its DTLS certificate fingerprint. All tracks share one transport (BUNDLE) and RTCP rides on the same port.
- ICE connectivity checks reach the SFU over UDP, and a DTLS handshake on the same port derives the SRTP keys. A STUN binding request carrying the ICE credentials proves the path works and that this is the client the SFU was told to expect. DTLS then takes one or two round trips, so on a 20 ms path the whole exchange is well under 200 ms. Consent checks repeat every few seconds and keep the NAT binding open.
- If UDP is blocked, ICE falls back to TCP 443 at the SFU or to a TURN relay over TLS. The client tries UDP first and starts the TCP and TURN candidates in parallel after a short delay, so a blocked network costs about a second rather than a timeout. TURN over TLS on 443 looks like HTTPS to a corporate proxy. The relays sit in the same sites as the SFUs, so they add a millisecond; the real cost is that this participant gets TCP’s behaviour on loss.
- The client starts sending; the meeting announces the new participant and others start receiving it. Audio is Opus at ~32 kbps in 20 ms packets; video is 180p at ~150 kbps, 360p at ~500 kbps and 720p at ~1.5 Mbps, about 275 packets a second in all. Other clients update their layout from the roster event and the SFU starts forwarding each of them the layer they can use. Typical join to first audio is about a second.
- A receiver’s bandwidth drops mid-meeting. The core read path: the SFU, not the sender, decides what each receiver gets. When one participant’s Wi-Fi degrades, only that participant’s streams get smaller; nobody else in the meeting notices.
- The client tells the SFU what it is showing: one large tile and eight thumbnails. The layout sets the ceiling: a 180-pixel-high thumbnail never needs the 720p layer, whatever the bandwidth. Constraints go on a data channel over the same DTLS connection, so they reach the SFU in one hop instead of through signaling.
- The receiver sends congestion feedback, and the SFU estimates its downlink. Every packet the SFU sends this receiver carries a transport-wide sequence number, and the receiver reports when each arrived every 50 to 100 ms. The SFU runs a delay-based estimator: when packets arrive further apart than they were sent, a queue is building on the path, so it backs off before loss starts. Loss is the backstop signal, not the main one.
- The estimate falls from 3 Mbps to 1.2 Mbps; the SFU re-plans this receiver’s streams in priority order. Audio for the top three speakers first (~100 kbps), then the large tile, then thumbnails. 720p plus eight 180p thumbnails is ~2.8 Mbps, so the large tile drops to 360p (~500 kbps) and the thumbnails drop to 7.5 fps by forwarding only their base temporal layer (~60 kbps each): about 1.1 Mbps in all. Nothing changes for any other receiver.
- To switch the large tile to 360p, the SFU waits for a keyframe on that layer, asking the sender for one if needed. Simulcast layers are independent streams, so a receiver can only start decoding a new layer at a keyframe. The SFU sends a picture loss indication for that layer at most once every ~500 ms, so fifty receivers switching at the same moment trigger one keyframe, not fifty. Until it arrives, the receiver keeps getting the old layer, so nothing freezes.
- Video packets lost on the last hop are re-sent from the SFU’s own cache. The receiver NACKs each missing video packet; the SFU keeps the last second of packets per stream and retransmits from there, so the repair crosses only the last leg instead of going back to the sender. That only helps when the round trip is under ~100 ms; beyond that the receiver asks for a keyframe instead. Audio is not retransmitted: Opus FEC and loss concealment cover it.
- When the link recovers, the estimate climbs slowly and layers step back up one at a time. Going up is deliberately slower than going down: the SFU probes for spare capacity with padding before committing to a bigger layer, and waits a few seconds between steps. Flapping between 360p and 720p every second looks worse than staying at 360p.
- A 1,000-person all-hands on three continents. The scale-breaking case. Forwarding everything to everyone would be 150 Mbps per receiver; last-N selection, cascaded regional SFUs and one owner for meeting state keep each receiver near 3 Mbps and each stream crossing an ocean once.
- The meeting gets SFUs in each region its participants are joining from. The invite list says ~1,000, so the allocator spreads the meeting across several SFUs per region instead of packing it onto one: a single server could carry the ~2.8 Gbps, but losing it would drop everyone at once. Each SFU is told which other SFUs belong to the meeting.
- Regional SFUs link over the backbone and subscribe on behalf of their local participants. The London SFU requests each remote stream once, at the highest layer any of its local receivers needs, and fans it out locally. Nine video senders at up to three layers is about 20 Mbps per link per direction, small next to the ~2.8 Gbps delivered to receivers inside the regions. The backbone has far less loss and jitter than the public internet, so the extra hop costs a few milliseconds and buys stability.
- Audio levels from every SFU produce one meeting-wide speaker ranking; each receiver gets the top three voices and the last nine speakers’ video. Clients tag every audio packet with its level, so ranking needs no decoding. SFUs share their local senders’ levels a few times a second and all compute the same order, with about a second and a half of hysteresis so a cough does not steal the large tile. The other 990 participants cost almost nothing: muted microphones with discontinuous transmission send a few packets a second, and nobody subscribes to their video, so their layers are paused.
- The CEO shares her screen, and it becomes the highest-priority video for every receiver. Screen content is encoded for sharp text at a low frame rate, typically 1080p or more at 5 to 15 fps and 1 to 2 Mbps, with the encoder tuned for detail rather than motion. The SFU allocates it right after audio, so a receiver on a weak link sees readable slides and a smaller speaker rather than the reverse.
- Someone asks a question in chat; the meeting’s owner sequences it and fans it out to 1,000 people. One owner per meeting gives chat a single order: it assigns the next sequence number, appends the message to the meeting’s chat log, and publishes it to the gateways holding this meeting’s participants, which write it to each WebSocket. One message becomes 1,000 writes spread over the gateways, so chat is rate-limited per participant and hosts can restrict who may message everyone.
- A late joiner gets the current state in one message, then receives only the active streams. The join response carries the roster, who is sharing, the current speaker and the last 100 chat messages from the live state store, so the newcomer never replays the meeting’s history. Roster updates to everyone else are batched every few hundred ms during join spikes: 1,000 people joining at 10:00, each announced to everyone, would otherwise be a million individual messages.
- An SFU dies mid-meeting. The failure path. Media state on the SFU is lost, but meeting state never lived there, so recovery is a media reconnect over the still-open signaling connection, not a rejoin. Target: voices back in about three seconds.
- The SFU stops heartbeating, and the allocator declares it dead when its lease expires. SFUs report health and load every second with a 3-second lease. Clients notice independently: no RTP or RTCP for ~2 seconds while their WebSocket is still up gets reported to the meeting service, which also catches a hung process whose heartbeat thread still runs.
- For each affected meeting, the owner gets replacement capacity spread across the region and records the new assignment. The dead server carried ~680 meetings and ~3,400 participants. The allocator spreads them across many servers so no single neighbour takes the whole load, which is what the 30 % headroom is for. The new mapping is written with a bumped epoch, so any late message based on the old assignment is ignored.
- The meeting service tells each participant over signaling to reconnect its media. The WebSocket goes to a gateway, not to the SFU, so it survived the crash. That is the whole point of keeping signaling and media on separate paths.
- Each client performs an ICE restart against the new SFU, publishing audio before video. New ICE credentials and a new DTLS handshake, then the client republishes its tracks and re-subscribes from the roster it already holds. Clients add up to a second of random jitter, so 3,400 reconnects arrive spread out instead of in one burst. Audio first means voices return before video does.
- Receivers need keyframes for every video stream; the new SFU deduplicates the requests. Every receiver’s decoder lost its reference frames, so each needs a keyframe from every sender it watches. The SFU sends one keyframe request per sender layer and holds the rest; otherwise a 200-person meeting would ask each speaker for 200 keyframes in the same second.
- The recovery is recorded per participant, as time from failure to first audio. The target is voices back in ~3 seconds at p50 and under 5 at p99: about 2 seconds to detect and 1 to reconnect. The events feed dashboards and the postmortem; a slice of participants that took 20 seconds usually points at one client version with bad reconnect logic.
- Cloud recording, from bot to playback. The asynchronous path. A bot joins like any participant and writes raw tracks to object storage; the expensive decoding and encoding happen after the meeting on batch capacity, so recording never slows the call down.
- The host starts recording; the meeting service launches a recording bot near the meeting’s SFU. Recording is a meeting state change, applied by the owner and broadcast so every client shows the recording banner. The bot comes from a warm pool in the same region, so it is in the meeting within a couple of seconds.
- The bot joins the SFU as a receive-only participant and subscribes to every audio track, the speaker’s video and the screen share. To the SFU the bot is one more subscriber, so there is no new code in the forwarding path. It asks for the 720p layer of whoever is on stage and the full-resolution share, whatever other receivers get. Separate audio tracks let the composite balance levels and let a transcript say who said what.
- The bot writes each track to object storage in 10-second chunks, without decoding. Frames are depacketised into a container, with RTP timestamps mapped to wall-clock time from RTCP sender reports so tracks can be aligned later, and a manifest lists chunks and gaps. A crashed bot loses at most the chunk in progress, and its replacement continues with a new chunk sequence.
- When recording stops, the meeting service publishes an event and a composite job picks it up. Compositing is batch work: it can wait seconds for capacity and run on cheaper or spot machines. The event carries the manifest location, and the job is idempotent on recording id, so a redelivered event never produces two files.
- The job aligns and decodes the tracks, renders speaker view with the share, and encodes MP4, audio-only and HLS. An hour-long recording is split into 5-minute pieces encoded in parallel, so it is ready in minutes rather than in an hour. Output runs at ~1.1 Mbps, about 0.5 GB per hour; at 1 M recorded hours a day that is ~500 TB of new storage daily, which is why retention defaults matter.
- The host opens the recording and the player streams it from the CDN with a signed URL. Recorded playback is ordinary video on demand: HLS segments cached at the edge and adaptive bitrate in the player. The URL is signed, expires within hours and is issued only after checking the account’s sharing settings, so a leaked link stops working. Deleting a recording removes the objects and purges the CDN.
Deep dives
Mesh vs MCU vs SFU
Why not connect participants directly to each other, or mix everyone into one stream on a server?
Every participant produces audio and video that every other participant may want. In a full mesh each of N people sends N − 1 copies: in a 10-person meeting at 1.5 Mbps per 720p stream, that is 13.5 Mbps of uplink per laptop, and nine separate encodes if each copy is to adapt to its receiver. Home uplinks are often 10 to 20 Mbps, and a laptop cannot encode nine 720p streams in real time. The opposite extreme, an MCU, decodes everything on a server, composes a picture per receiver and re-encodes it: roughly a CPU core per real-time 720p encode, and 50 to 150 ms of extra delay in a 150 ms budget.
The observation that shapes the answer: forwarding packets is cheap and decoding them is expensive. A server that receives each sender’s stream once and copies the packets to whoever needs them, without decoding, keeps every uplink at one stream and adds only a few milliseconds. What it gives up is that receivers decode several streams, so the server has to choose carefully which streams, at which quality, each receiver gets.
- SFU: receive once, forward packets without decoding chosen
- Peer-to-peer mesh situational: Two-person calls where ICE finds a direct path: no server bandwidth and the shortest route. Move to the SFU as soon as a third person joins or recording starts.
- MCU: decode, compose and re-encode per receiver rejected
- Mix audio only, forward video situational: Phone dial-in and SIP room systems that can take only one audio stream: a gateway mixes the top speakers for them while everyone else stays on the SFU.
The answer: An SFU for every meeting. Each client sends one Opus audio stream and three simulcast video layers to an SFU in its nearest region; the SFU terminates DTLS-SRTP per participant, never decodes, and forwards each packet to the subscribers that want it, rewriting SSRC, sequence number and timestamp so a layer switch looks like one continuous stream to the receiver. One server forwards about 10 Gbps, roughly 1 M packets a second, and is run to about 70 % of that, ~3,500 participants. Two-person calls could go peer to peer when ICE finds a direct path, but I would start SFU-only, because one media path is simpler to operate and recording and late joiners need the server anyway. Endpoints that can take only one stream, such as phones and old room systems, join through a gateway that mixes audio. Zoom has said its clients speak its own protocol over UDP to SFU-like multimedia routers, and the open-source WebRTC media servers (Jitsi Videobridge, mediasoup, Janus) are SFUs; the internals here are my design, not a description of Zoom’s.
Most meetings have two or three people. Is the SFU wasted on them?
It costs two server streams per 1:1 call, about 4 Mbps of server bandwidth, and a few milliseconds. Peer to peer saves that when ICE finds a direct path, but it needs TURN when it does not, it exposes each side’s IP address to the other, and a third person joining forces a renegotiation onto the SFU with a second or two of glitch. Recording and late joiners need the server anyway. I would ship SFU-only and add peer to peer for 1:1 only if the bandwidth bill says a second media path is worth it.
What does the SFU actually do to each packet?
Receive it on the sender’s transport, decrypt it with that sender’s SRTP key, read the RTP header and the extensions it routes on (audio level, layer ids, transport sequence number), and decide which receivers get it. For each receiver it rewrites SSRC, sequence number and timestamp so layer switches look continuous, encrypts with that receiver’s key and sends. It also keeps the packet in a one-second cache for retransmissions. The per-receiver encrypt and send is the main cost, which is why servers are sized in packets per second.
Can you do end-to-end encryption with an SFU in the middle?
Yes. The client encrypts each encoded frame with a meeting key before packetising it (SFrame, or the browser’s encoded-transform API), so the SFU forwards ciphertext while still reading the RTP headers and extensions it routes on. Clients distribute the keys and rotate them when someone joins or leaves. The price is every server-side feature that needs the media: cloud recording, transcription, phone gateways. Zoom offers end-to-end encryption as an option that disables cloud recording for exactly this reason.
When would you build an MCU after all?
For endpoints that can take only one stream: old SIP or H.323 room systems, phone dial-in for audio, or an RTMP push to a live-streaming platform that wants one composed picture. I would build it as a gateway that joins the meeting as a participant, subscribes through the SFU and mixes for its one endpoint. Only meetings that use it pay for the decoding and encoding, and the core path stays a forwarder.
How do you know an SFU is reaching its limit?
Watch packets per second and CPU per core, not only gigabits: a small audio packet costs as much work as a large video one. Socket receive-buffer drops and growing forwarding delay inside the server are the early signs. The allocator stops placing meetings at about 70 % of the packet and egress budget, and a server past 85 % sheds its lowest-priority work first, such as thumbnail layers, before audio is touched.
What each receiver gets
Receivers range from fibre to a phone on a train, and a 1,000-person meeting has 999 possible streams. What does the SFU send each one?
A receiver’s downlink can be 100 Mbps or 800 kbps, and on Wi-Fi or cellular it changes second to second. Sending one quality to everyone means either the weakest receiver freezes or the strongest gets 180p. And in a 1,000-person meeting, forwarding every camera even as a 150 kbps thumbnail is 150 Mbps per receiver and 150 Gbps for the meeting: no client can take that, and nobody can look at 999 tiles.
Two observations shape the answer. A receiver displays only a handful of tiles, typically one large and up to 24 small, and only two or three people speak at once, so what to forward is bounded by the layout and by voice activity, not by meeting size. And the SFU is the sender on every downlink, so it is the natural place to measure each receiver’s bandwidth and pick a quality per stream, as long as senders give it layers to pick from.
- Last-N by voice activity, simulcast layer per receiver from layout and estimated bandwidth chosen
- SVC: one stream with spatial and temporal layers situational: Senders whose encoder supports it (VP9 or AV1 SVC, usually in software). Temporal layers inside each simulcast stream are worth having everywhere, since dropping them halves the frame rate without a keyframe.
- Forward everything; each client drops what it cannot show rejected
- The sender encodes once, for the weakest receiver rejected
The answer: Senders publish three simulcast layers (180p at ~150 kbps, 360p at ~500 kbps, 720p at ~1.5 Mbps), each with three temporal layers (7.5, 15 and 30 fps), and tag every audio packet with its level in the RFC 6464 header extension. The SFU ranks speakers meeting-wide from those levels with ~1.5 s of hysteresis, forwards audio from the top three, and forwards video from the last-N most recent speakers (9 by default, up to 25 in gallery view) plus pinned and screen-share tracks; layers nobody subscribes to are paused at the sender. For each receiver it runs a delay-based bandwidth estimator on transport-wide congestion control feedback and spends the estimate in priority order (audio, screen share, the large tile, then thumbnails), choosing the highest spatial and temporal layer that fits and never more than the tile’s displayed size. A new spatial layer starts at a keyframe, requested with a PLI at most once per sender layer every ~500 ms. The same estimator runs on each uplink with the SFU sending the feedback, so a congested sender lowers its bitrate and drops its top layer. The result is ~2.8 Mbps per receiver for a full 9-tile view, whatever the meeting size.
REMB or transport-cc: which would you use, and why?
With REMB the receiver runs the estimator and sends the sender a single number, the bitrate it thinks it can take. With transport-cc the receiver only reports when each packet arrived, every 50 to 100 ms, and the sender runs the estimator. Transport-cc puts the logic where the decisions are made, on the SFU for downlinks, lets it improve without updating every client, and covers all streams on the transport at once. I would use transport-cc and keep REMB for old clients only.
The CEO starts talking and 500 receivers switch her to the large tile at once. What happens?
Each needs a keyframe of her 720p layer before it can decode it. Passing on 500 requests would make her emit keyframes back to back, each 5 to 10 times a normal frame, and congest her own uplink. The SFU deduplicates: one PLI per sender layer at most every ~500 ms, and every waiting receiver switches at the same keyframe. Until it arrives they keep her 360p layer, so nothing freezes; remote SFUs ask her local SFU rather than her.
How does the SFU pick the active speaker without decoding audio, and stop a cough from taking the stage?
Each audio packet carries its level in a header extension, so the SFU reads a number instead of decoding Opus. It smooths levels over a few hundred milliseconds, and a new speaker must be louder than the current one for about 1.5 seconds to take the large tile. A cough reaches the top-three audio, which is fine, but not the stage. Client-side noise suppression keeps keyboards from scoring as speech, and a host can spotlight someone to pin the stage.
A receiver’s bandwidth collapses to 300 kbps on a train. What do they get?
Audio first: the top three speakers at ~100 kbps together. Then the speaker at 180p, dropping to 15 or 7.5 fps if needed, with thumbnails suspended in favour of avatars; a screen share, if there is one, comes before the speaker’s face. If even audio struggles, Opus lowers its bitrate and leans on FEC, and the client tells the user their connection is unstable. Nobody else in the meeting sees any change.
Why not let each client choose its own layers?
The client knows its layout but not its bandwidth as well as the SFU, which is the sender and sees every feedback report. The SFU also knows which layers exist, which are paused and when the next keyframe is due. So the client sends constraints (which tiles, at what maximum size) and the SFU makes the allocation. That keeps the decision in one place and lets it react within milliseconds.
UDP, packet loss and firewalls
Why not send media over TCP like everything else? And what happens when packets are lost or UDP is blocked?
TCP delivers bytes in order, so one lost packet holds back everything behind it until the retransmission arrives: at least one round trip, and 200 ms or more after a timeout. For a download that is invisible; in a call it is a freeze followed by a burst of stale audio. A 20 ms audio frame that arrives 300 ms late is worthless, and a video packet repaired after its frame was due on screen is wasted bandwidth.
So media goes over UDP, and the system handles loss, reordering and jitter itself, deciding per packet whether to repair, conceal or skip it based on whether a repair can arrive before the packet must play. And because some corporate, hotel and school networks block UDP, there has to be a way to carry the same packets through TCP port 443 when nothing else works.
- RTP over UDP with DTLS-SRTP, adaptive jitter buffer, NACK, FEC and concealment chosen
- TCP or WebSocket for media situational: Only as the last-resort fallback: ICE-TCP to the SFU on 443, or TURN over TLS through a proxy, for the few percent of participants whose network blocks UDP. Expect more freezes on lossy links.
- QUIC datagrams (WebTransport) situational: Native clients, or browsers once WebTransport and WebCodecs are the norm for media: unreliable datagrams over QUIC behave like UDP, with encryption built in and on port 443.
- Retransmit every lost packet rejected
The answer: Media is RTP over UDP, encrypted with SRTP using keys from a DTLS handshake on the same port, with all tracks and RTCP multiplexed on one transport per participant. The receiver’s adaptive jitter buffer holds 20 to 60 ms and reorders packets. Audio (Opus, 20 ms frames, 50 packets a second) relies on in-band FEC and packet loss concealment, because a retransmission rarely beats its deadline. Video uses NACK when the round trip is under ~100 ms, answered from the SFU’s one-second packet cache so the repair crosses only the last hop, adds FlexFEC when loss persists on long paths, and falls back to a keyframe request when a frame cannot be repaired. For connectivity, the SFU is ICE-lite on public IPs and offers UDP and TCP 443 candidates, so most clients reach it directly; a TURN fleet in the same sites relays TURN over TLS on 443 for networks that allow nothing else, using short-lived credentials issued at join.
How big should the jitter buffer be?
As small as the network allows, and adaptive. It starts at 20 to 40 ms, grows when packets arrive too late to play, and shrinks slowly by playing audio imperceptibly faster, which is how WebRTC’s NetEQ works. Every 10 ms it holds is 10 ms more mouth-to-ear delay, so the target is a small late-loss rate, around 1 %, not zero. A buffer sized for the worst jitter of the day makes every call laggy.
What fraction of users need TURN, and what does it cost?
With SFUs on public IPs and a TCP 443 candidate, only networks that force traffic through an HTTPS proxy need it, typically a few percent of participants, concentrated in some enterprises. Each relayed participant costs relay bandwidth both ways, about 5 Mbps, and gets TCP’s behaviour on loss. Putting TURN in the same sites as the SFUs keeps the extra hop to about a millisecond. I would track the relay share per customer and publish our IP ranges and ports so their IT teams can allow UDP, as conferencing vendors do.
A user walks out of the office and switches from Wi-Fi to cellular mid-call. What happens?
The client’s address changes, so packets to the old one go nowhere. The client sees the network change and does an ICE restart: new credentials and candidates in a fresh offer over the WebSocket, connectivity checks against the same SFU, and media resumes on the new path in about a second. The SFU keeps the participant’s session for ~30 seconds, so subscriptions survive. The bandwidth estimate restarts low, because cellular is a different link.
How do you stop the TURN servers being used as an open relay?
Credentials are short-lived: the join response carries a username that embeds an expiry and a password that is an HMAC of it with a secret the TURN servers share, so nothing is stored per user and stolen credentials expire on their own. The servers relay only to SFU address ranges, never to arbitrary hosts, and rate-limit per credential. That way TURN cannot be used to reach the internet or anything internal.
Why do audio and video use different repair strategies?
Audio packets are tiny, 50 a second, with a deadline tens of milliseconds away; FEC costs little and concealment hides a single lost 20 ms frame almost perfectly. A video frame spans several packets and later frames reference it, so one loss corrupts the picture until the next keyframe, and repairing it is worth a retransmission when the round trip allows. Video FEC costs 10 to 30 % more bandwidth, so it is switched on only when loss is persistent and the round trip is long.
Placing meetings and cascading SFUs
Participants in New York, London and Singapore join the same meeting. Which server does each connect to?
Distance is latency. Capture, encode, jitter buffer and decode take about 80 ms, and a client within ~20 ms of its SFU on each side lands at ~120 ms, inside the 150 ms budget. New York to Singapore is ~115 ms one way, so with a single SFU in New York, two people in Singapore talking to each other would hairpin across the world and back: ~310 ms. The long legs would also cross the public internet, where loss and jitter on a 230 ms round trip cannot be repaired in time.
So each participant should connect to an SFU near them, and a meeting’s SFUs should be linked over our own backbone, carrying each stream between regions once rather than once per remote viewer. Most meetings are a few people in one place, so this must cost nothing when it is not needed. Within a region the allocator also decides how to spread meetings over servers: packing a meeting onto one server avoids internal hops, but a 1,000-person meeting or a server failure should not take everyone at once.
- Nearest-region SFU per participant, cascaded over the backbone chosen
- One SFU per meeting in the first joiner’s region situational: All participants are in one region, which is most meetings: one SFU is all there is, and the cascade appears only when someone joins from far away.
- A few central data centres, reached over the internet rejected
The answer: At startup the client measures RTT to probe endpoints in a few nearby regions and sends the results with its join; the meeting service asks the allocator for an SFU in the lowest-RTT region. Within a region the allocator puts a small meeting on one server, chosen by headroom in egress, packets per second and CPU and never past ~70 %, and spreads meetings expected over ~200 people across several. When someone joins from a region the meeting does not yet use, the allocator adds an SFU there and the meeting service links it to the others: each SFU subscribes once per remote stream, at the highest layer any local receiver needs, and fans it out locally. Cascade links run over the private backbone with their own transport-cc estimation, and SFUs exchange audio levels so every region computes the same speaker ranking. Account policy can restrict a meeting to allowed regions for data residency; Zoom, for example, lets admins choose which data centre regions carry their meetings.
How does the London SFU know which streams to request from New York?
It aggregates what its local receivers want: for each remote sender, the highest layer any local allocation needs, and the union of their last-N lists. It sends that as one subscription over the cascade link and updates it when local demand changes. If the link itself gets congested, the New York SFU lowers layers for the whole London region at once, and London’s SFU allocates what arrives among its receivers.
A 1,000-person all-hands: 600 in the US, 300 in Europe, 100 in Asia. How much crosses between regions?
Per stream per region, not per viewer. Each region needs at most the last-N video, nine senders at up to three layers, about 20 Mbps, plus a few audio streams, per direction on each link. Three regions linked pairwise is six directional links, around 120 Mbps in total. The 2.8 Gbps of delivery to the 1,000 receivers happens inside each region.
Should a meeting’s SFUs form a full mesh or a tree?
A full mesh between regions while there are few of them, typically five or fewer, because every extra relay hop adds delay and a failure point, and one hop between any two regions is the minimum. Within a region, several SFUs serving one big meeting can relay through each other on the local network, where a hop costs well under a millisecond. A tree with a hub only makes sense for many small regions, or when one region’s links are poor.
A customer’s meetings must stay in the EU. What changes?
The allocator filters candidate regions by account policy before ranking by RTT. A participant joining from New York then connects to Frankfurt or Dublin and accepts ~30 ms more each way, and nothing is cascaded to a US SFU. Recordings go to an EU bucket and are processed by EU composite workers. The join response says which region was chosen, so the client can explain the latency.
How would you know placement is working?
Per participant: RTT to its SFU, estimated mouth-to-ear latency, and loss and jitter on each leg, sliced by country and ISP. The share of participants more than ~50 ms from their SFU shows where a new site would help most. Comparing the probe RTTs clients sent at join with what they then experienced shows whether the probes are choosing well.
An SFU dies mid-meeting
The SFU carrying a 200-person meeting crashes. What do people experience, and how does the system recover?
An SFU holds per-participant state that cannot be moved after a crash: ICE and DTLS sessions, SRTP keys, bandwidth estimates, packet caches and subscriptions. With ~3,500 participants on a typical server, one crash hits thousands of people in hundreds of meetings, and they all try to recover in the same second. The meeting itself (who is in it, who is host, who is muted, the chat) has to survive, which means it must not live on the media server.
The separation that makes this tractable: signaling and meeting state are the control plane, and the SFU is replaceable plumbing. If the meeting service owns the roster and the meeting-to-SFU map, and every client’s WebSocket goes to a gateway rather than to its SFU, then losing an SFU means a media reconnect, not a rejoin. What remains is how quickly the failure is detected and how the reconnect storm is spread out.
- Detect, reassign, and have clients ICE-restart to a new SFU over signaling chosen
- Hot standby SFU receiving a copy of every stream rejected
- Replicate SFU state to a peer that takes over its address rejected
- Clients send audio to two SFUs situational: Very high-stakes meetings such as a company all-hands: audio is only ~32 kbps, so duplicating it lets voices survive a crash instantly while video recovers normally.
The answer: Meeting state lives with the meeting’s owner in the meeting service and is persisted on every change to the live state store under an epoch; SFUs hold only media state. SFUs heartbeat to the allocator every second with a 3-second lease, and clients separately report when no RTP or RTCP has arrived for ~2 seconds while their WebSocket is up; whichever comes first triggers recovery. The owner asks the allocator for replacement capacity in the same region, spread across many servers so no neighbour takes the whole load, and writes the new meeting-to-SFU map with the epoch bumped. It then sends media.reconnect over each participant’s WebSocket; clients ICE-restart to the new SFU with up to a second of random jitter, publish audio before video and re-subscribe from the roster they already hold, while the new SFU deduplicates the burst of keyframe requests. Voices return in about 3 seconds at p50 and under 5 at p99. Planned moves for deploys and draining are make-before-break: the client connects to the new SFU before leaving the old one, so nobody hears a gap.
A signaling gateway holding 50,000 WebSockets restarts. What happens to the calls?
Nothing audible: media goes to the SFUs, not the gateway. Clients reconnect their WebSocket to another gateway with backoff and jitter, resume with their session id and the last event sequence they saw, and the meeting service replays any roster or chat events they missed. Users may see chat arrive a second late. That asymmetry is exactly why signaling and media take separate paths.
The meeting’s owner instance dies instead. What happens?
Media keeps flowing, because SFUs do not need the owner to forward packets. Joins, mutes and chat pause for a few seconds while another instance takes the lease, loads the meeting from the live state store and carries on. The epoch fences the old owner: if it was only paused, say in a long garbage collection, its writes and its commands to SFUs carry the old epoch and are rejected. Clients resend unacknowledged actions with their ids, so nothing is applied twice.
A whole region’s SFUs become unreachable.
The same mechanism one level up: the allocator places the affected meetings in the next-lowest-RTT region using the probe results clients sent at join, and the owners send media.reconnect pointing there. Latency rises for those users, but the meetings continue. If signaling in that region is down too, clients reconnect their WebSocket through the global front door to another region and resume there. Each region keeps headroom for a neighbour, and beyond that the system refuses new meetings before it drops existing ones.
How do you know the failover actually works?
Kill SFUs in production on purpose, routinely, and measure time from failure to first audio per participant at p50 and p99, by client version. Run synthetic meetings with bot participants in every region around the clock, so recovery is measured even at night. A client release whose recovery time jumps is the usual culprit, so recovery time is a release gate for clients.
What happens to a recording when the SFU it subscribes to dies?
The recording bot is a participant like any other, so it gets media.reconnect and moves to the new SFU. The few seconds without media show up as a gap in its chunk manifest, with wall-clock times on both sides. The composite job fills the gap with silence and the last frame, or a short slate. Everything uploaded before the crash is already safe in object storage.
Cloud recording without slowing the call
How do you record meetings to the cloud without slowing down the call?
Recording sits next to a real-time system with no slack. Anything added to the SFU’s forwarding loop, such as decoding, mixing or disk writes, takes packet budget from thousands of other people’s calls. And the volume is large: at 1 M recorded hours a day and ~0.5 GB per finished hour, that is ~500 TB of new storage a day, so the format, the processing and the retention policy all matter.
The insight: a recorder is just another participant that never sends. It can subscribe to exactly the tracks it needs, at the quality it wants, from the same SFU. And the expensive part, decoding and re-encoding into one file, does not have to happen in real time at all: it can run after the meeting on cheap batch capacity.
- Bot participant writes raw per-track chunks; composite and transcode after the meeting chosen
- Composite in real time with an MCU-style recorder situational: The meeting is also being live-streamed (RTMP out, or HLS for a large audience), so a composed stream exists anyway; record that stream.
- Record on the host’s computer situational: Free tiers and privacy-sensitive accounts: local recording costs us nothing, which is why Zoom offers it alongside cloud recording.
- Write packets to disk inside the SFU rejected
The answer: When a host starts recording, the meeting service launches a recording bot from a warm pool in the meeting’s region; it joins like a client with a service credential and appears in the roster, so every participant sees the recording banner. It subscribes to every audio track, the active speaker at 720p and the screen share at full resolution, and writes frames to object storage in 10-second chunks without decoding, mapping RTP timestamps to wall-clock time from RTCP sender reports so tracks can be aligned; a manifest records chunks and gaps. When recording stops, the meeting service publishes recording.stopped on the event bus, and a composite job, idempotent on recording id, decodes and aligns the tracks, renders speaker view with the share, and encodes MP4 (~1.1 Mbps), audio-only and HLS renditions, splitting long recordings into 5-minute pieces encoded in parallel. Playback is ordinary video on demand from a CDN with signed, expiring URLs. Raw chunks are deleted a week after a successful composite; finished recordings are encrypted at rest and follow the account’s retention policy.
The recording bot crashes 40 minutes into a one-hour recording.
Everything uploaded so far is durable, so at most the 10-second chunk in progress is lost. The meeting service sees the bot’s WebSocket drop and, because recording is still on in the meeting state, launches a replacement that joins and continues with a new chunk sequence. The composite job sees the gap in the manifest and fills it with silence and the last frame. Bot restarts per recorded hour is a metric worth alerting on.
How quickly is a one-hour recording ready, and how would you speed it up?
Decoding, compositing and encoding an hour on one machine with a fast preset takes on the order of 15 to 30 minutes. Splitting it into 5-minute pieces at chunk boundaries and encoding them in parallel brings that down to a few minutes, the same trick video platforms use for uploads. The audio-only file and the transcript are cheap, so they come first and the most-used outputs appear soonest.
Why record every audio track separately instead of the mixed top three?
Separate tracks let the composite balance levels and mix properly, let a transcript attribute words to speakers, and make sure a quiet participant is never lost because they were outside the live top three. With discontinuous transmission, silent and muted tracks send almost nothing, so recording all of them costs little even in a 1,000-person meeting.
How do participants know they are being recorded, and who can watch it?
The bot is in the roster and recording is meeting state, so every client shows a banner and new joiners are told as they arrive; some jurisdictions also need an explicit consent prompt. The recording belongs to the host’s account and is shared through signed, expiring CDN URLs, optionally with a passcode. Deleting it removes the objects and purges the CDN, and a legal hold blocks deletion for accounts under one.
Storage grows by 500 TB a day. How do you keep the bill down?
Retention by default: recordings expire after an account-set period unless someone keeps them, so live storage is bounded at roughly 500 TB times the retention in days, about 45 PB for 90 days. Move recordings to an infrequent-access tier after 30 days, since most are watched in the first week or never. Delete raw tracks after the composite and render gallery view only when asked; at this scale every rendition kept by default is a line item.
The theory behind it
- Video calls and WebRTC: How Zoom-style video calling works: WebRTC media and signaling, NAT traversal with STUN and TURN, peer-to-peer versus SFU versus MCU, simulcast and adaptive quality, scaling large meetings with cascaded SFUs, recording, latency budgets and reliability.
- Networking for system design: DNS, TCP and UDP, HTTP/1.1 versus HTTP/2 and HTTP/3, TLS, proxies and reverse proxies, and the latency numbers behind them, at the depth a system design interview expects.
- Global traffic management, DNS and anycast: How users reach the nearest healthy region: DNS resolution and TTLs, GeoDNS and latency-based routing, anycast, global load balancers, health checks and failover, the limits of DNS-based failover, and steering traffic during incidents and migrations.