Engineering Atlas › System Design Interviews

Design a video conferencing system

22 min read · last reviewed 2026-10-17

Open in the interactive app: practice, quiz, mock interview →

In one minute

Requirements

Functional

Non-functional

Out of scope: webinars of 10,000+ viewers (served as a one-to-many live stream, see Media Streaming), breakout room logic, transcription and AI summaries, billing.

Back-of-envelope

Round assumed numbers, not Zoom's published figures.

API

POST /v1/meetings                      { topic, start_time, settings }  -> { meeting_id, join_url }
POST /v1/meetings/{id}/join            { display_name, passcode }
  -> { participant_id, token, signalling_url, media: [ { region, host, port } ] }

Signalling (WebSocket, JSON shown):
-> publish     { kind: "video", encodings: [ { rid: "q", kbps: 150 }, { rid: "h", kbps: 500 }, { rid: "f", kbps: 1500 } ] }
-> subscribe   { publisher_id, max_height: 360 }         tile size drives the layer cap
<- roster      { joined: [...], left: [...], version: 412 }
<- speakers    { active: ["p17", "p03", "p44"] }
-> control     { action: "mute", target: "p22" }           host only

Media (UDP to the SFU, TCP or TLS on 443 as fallback):
  SRTP packets (RTP header + encrypted payload), RTCP feedback (NACK, PLI, receiver reports, bandwidth estimates)

The join response lists candidate media servers so the client can probe them and pick the fastest. subscribe carries the tile size, not a bitrate: the client knows its layout, the SFU knows the network, and the SFU combines both.

Data model

meetings (relational, partitioned by meeting_id): host_id, passcode_hash, settings (waiting room, recording, E2EE), scheduled_start, recurrence. Read on every join, rarely written, so it caches well.

live_sessions (a strongly consistent registry such as etcd, or a replicated key-value store with leases): meeting_id -> { controller_id, sfus: [ {sfu_id, region} ], roster_version }. Leased, so a crashed controller's meetings are reassigned when the lease expires. This is the only shared state for a running meeting; it is small and short-lived.

Roster and subscriptions live in memory on the meeting's controller and on its SFUs: who is publishing which layers, who subscribes to whom and at what size. They are rebuilt from clients' re-joins if a node dies, so they need no database.

recordings: (meeting_id, recording_id) -> object_key, duration, size, status, expires_at, with the video itself in object storage. expires_at drives retention.

sfu_load (in memory, gossiped): per host, current forwarding Gbit/s, packets/s and participants, used for placement.

High-level design

flowchart LR
  C1["Client (Tokyo)"] -->|"HTTPS join"| API["Meeting API"]
  API --> REG[("Live session registry")]
  C1 <-->|"WebSocket signalling"| CTRL["Meeting controller"]
  CTRL --> REG
  C1 <-->|"SRTP over UDP"| S1["SFU Tokyo"]
  C2["Client (Frankfurt)"] <-->|"SRTP over UDP"| S2["SFU Frankfurt"]
  S1 <-->|"cascade, one copy per stream"| S2
  REC["Recording bot"] <--> S2
  REC --> OBJ[("Object storage")]
  PSTN["Phone gateway"] <--> S1

Join path.

  1. The client calls join. The API checks the passcode and waiting-room setting, looks up the meeting in the registry, and either finds its controller or creates the live session and places it.
  2. The response lists SFUs in the client's region and in regions already hosting the meeting. The client probes them over UDP and picks one; ICE handles NAT traversal (RFC 8445), with TURN relaying over TCP or TLS on 443 if UDP is blocked (RFC 8656).
  3. Over signalling, the client announces what it publishes and what it wants to see. The controller tells its SFU to wire up the subscriptions and, if the meeting spans regions, to open a cascade link.

Media path.

  1. The camera produces frames; the encoder emits three simulcast layers, packetized into RTP packets of about 1,200 bytes, encrypted with SRTP and sent to the SFU.
  2. The SFU decrypts the SRTP envelope (or, under E2EE, sees only headers: SFrame, standardized in 2024, encrypts whole media frames end to end while leaving the metadata an SFU needs for forwarding readable, RFC 9605), reads the RTP header and extensions, and for each subscriber decides whether this packet's layer is the one that subscriber should get.
  3. It rewrites sequence numbers and timestamps so each subscriber sees one continuous stream, re-encrypts for that subscriber's session and sends.
  4. Subscribers send RTCP feedback: NACKs for lost packets, keyframe requests, and bandwidth estimates that drive the next layer decision.

Deep dives

1. Topology: mesh, MCU or SFU

In a mesh, every participant sends to every other: with n people each uploads n minus 1 streams. Six people at 1 Mbit/s need 5 Mbit/s of upload each, which home links and phones cannot sustain. A mesh is right only for two or three people, where it also gives the lowest latency (one hop) and true end-to-end encryption for free.

An MCU decodes every stream, composes one mixed picture and re-encodes it per subscriber. Clients receive one stream, which suited old hardware, but the server pays a full decode and encode per meeting, adds 50 to 200 ms of processing delay, and every viewer sees the same layout.

An SFU only forwards. Upload is one stream (with layers); download is one stream per visible tile; server cost is packets per second, not codec work. Clients compose their own layout. I choose an SFU and keep MCU-style mixing for the edges that need one stream: phone dial-in gets a mixed audio stream, and cloud recording uses a bot that subscribes like a client and composes off the critical path. Zoom's own public material describes a multimedia router that forwards rather than transcodes, the same idea.

2. Layers and the per-subscriber bandwidth budget

Simulcast means the publisher encodes the same camera at several resolutions as separate streams, each sent as its own RTP stream identified by a RID (RFC 8851) and negotiated in SDP as described in RFC 8853. Scalable video coding (SVC, in VP9 and AV1) puts layers inside one stream: a base layer plus enhancement layers that depend on it, so the SFU drops higher-layer packets to step down. Simulcast costs more uplink and encoder CPU; SVC costs codec support and slightly worse compression per bit. Either way, the SFU has a menu per publisher.

For each subscriber, the SFU knows an estimated downlink from congestion control feedback (transport-wide sequence numbers and the receiver's arrival times, analysed with a delay-gradient model such as Google Congestion Control). Note that GCC never became a standard: its IETF draft expired in 2017 (IETF datatracker), and what browsers run is whatever version ships in libwebrtc. It allocates that budget across visible tiles in priority order: active speaker and pinned tiles first, then the gallery. First every visible tile gets its lowest layer if it fits, so nobody turns into a frozen avatar while the speaker is in HD; then tiles are upgraded one step at a time in priority order until the budget runs out or each tile reaches the largest layer its on-screen size can use. Off-screen tiles get nothing. The Practice section implements exactly this allocator.

Two details make it work in practice. Switching up a layer requires a keyframe on the new layer, so the SFU sends a PLI (picture loss indication) to the publisher and keeps forwarding the old layer until the keyframe arrives. And estimates are noisy, so the allocator adds hysteresis: move down at once when the budget drops, move up only after the higher budget has held for a few seconds.

3. Large and global meetings

A meeting with people in Tokyo, Frankfurt and São Paulo should not hairpin everyone to one region; 250 ms of round trip to a distant SFU leaves no latency budget. Each participant connects to the SFU in their own region, and SFUs cascade: Tokyo forwards each published stream once to Frankfurt, which fans it out locally. Inter-region traffic is then (publishers × regions), not (publishers × subscribers). For a 1,000-person meeting, the controller can also spread participants over several SFUs in one region, linked the same way, so no single host owns the whole meeting.

Audio has its own rule. With 1,000 people, forwarding 1,000 audio streams to each would be 40 Mbit/s of mostly silence. Clients put their audio level in an RTP header extension (RFC 6464), so the SFU can rank speakers without decrypting audio, and forwards only the loudest three or so. The same ranking drives the speakers signalling event that the client uses to pick the large tile, which in turn moves the bandwidth budget toward the speaker.

Variant: live streaming to millions of viewers

Take the same media-server ideas and point them at a broadcaster with a huge, mostly silent audience, as on Twitch. The requirements flip. Latency can relax from 150 ms to 2 to 5 seconds, because viewers do not talk back through the video, and in exchange audience size jumps from 1,000 to over a million per channel. Nothing is interactive, so the fan-out can ride ordinary HTTP caches instead of per-viewer UDP sessions.

The ingest path changes first. The broadcaster sends one stream over RTMP, SRT or WebRTC ingest (RFC 9725, WHIP) to a nearby point of presence, which carries it over a private backbone to an origin. The origin, not the viewer's device, does the transcoding: it decodes once and encodes a ladder of renditions with keyframes aligned, so a player can switch quality at a segment boundary. That compute is the main cost, so it is spent by expected audience: guaranteed ladders for channels likely to draw viewers, source quality only for the long tail. An SFU's per-subscriber layer choice becomes the player's own choice among prepared renditions.

Delivery becomes a pull tree. Packaged segments of about 2 seconds, cut into parts of roughly 0.3 to 0.5 seconds under Low-Latency HLS, are published to edge caches that fetch a stream only once a viewer asks. A player holds a blocking playlist request until the next part exists, so thousands of waiting viewers at one edge must be coalesced into a single upstream fetch. The latency budget is spent on a few seconds of player buffer rather than a jitter buffer of tens of milliseconds (HLS draft). Cost shifts from SFU packet rate to egress and transcode cores.

Failure handling changes too. Instead of re-publishing after an SFU dies, a broadcaster whose home link flaps stays in a reconnecting state for tens of seconds, and the same session resumes with a playlist discontinuity marker so viewers and chat keep their place.

Chat becomes the second fan-out system. One channel with a million viewers and 50 messages a second means 50 million deliveries a second. Moderation runs once per message before fan-out, a hierarchical publish and subscribe tree sends each message only to chat edges with a joined connection, edges batch lines into one frame every 100 to 200 ms, and a slow client has messages dropped instead of buffered. It shares only the channel ID with the video path.

Pick the Zoom design when participants talk to each other; pick this variant when one source feeds a very large, passive audience.

Down the stack

Layer 1The contract

  • Audio first. Under congestion the SFU degrades video (layer, then frame rate, then pause) before audio. Audio for the active speakers is forwarded whenever any media fits.
  • Latency over completeness. A packet that cannot arrive in time for playout is never delivered late. Loss is repaired by retransmission only when one round trip fits in the budget, otherwise concealed.
  • Per-stream continuity. A subscriber sees one continuous RTP stream per tile, even while the SFU switches layers underneath it.
  • Control plane is reliable. Roster changes, mutes and removals are ordered and acknowledged; a removal takes effect at the SFU within one second.

Layer 2The mechanism

Every RTP packet starts with a 12-byte fixed header: version, payload type, a 16-bit sequence number, a 32-bit timestamp and a 32-bit SSRC naming the stream (RFC 3550). Video timestamps use a 90 kHz clock, and Opus always uses 48 kHz whatever its actual sampling rate (RFC 7587). Header extensions carry the simulcast RID, the audio level and a transport-wide sequence number for congestion control. RTCP carries feedback: receiver reports with loss and jitter, NACK listing lost sequence numbers and PLI asking for a keyframe, both defined in the AVPF profile (RFC 4585), and bandwidth estimates.

The SFU's forwarding state per subscriber and source is small: the chosen layer, the offset applied to sequence numbers and timestamps since the last switch, and a ring buffer of recently sent packets to answer NACKs. On the client, a jitter buffer holds packets for 20 to 80 ms, sized adaptively from measured jitter, so that frames play out smoothly; Opus adds in-band forward error correction and packet loss concealment, so a lost 20 ms audio frame is reconstructed instead of heard as a click.

Layer 3The machine

  • Packet sizes. RTP payloads stay near 1,200 bytes so that IP, UDP, SRTP and any tunnel overhead fit in a 1,500-byte Ethernet MTU without fragmentation. A 1.5 Mbit/s stream at 30 frames/s is about 6 KB per frame, five packets per frame, about 160 packets/s. Opus at 20 ms frames is 50 packets/s of roughly 80 to 120 bytes each.
  • Packet rate is the bottleneck. At 520k packets/s, each packet gets under 2 microseconds of one core's time if handled serially, so SFUs batch with recvmmsg and sendmmsg, use UDP segmentation offload (GSO) on Linux, spread sockets over cores with SO_REUSEPORT, and some use kernel bypass. Per-packet SRTP decryption and re-encryption with AES-GCM costs well under a microsecond with AES-NI.
  • No codec work. The SFU never touches pixels, so a host's capacity is set by NIC queues and memory bandwidth, not by video resolution. An MCU doing the same job would need a core or more per 1080p decode and encode.
  • Latency budget. Capture and encode about 30 ms, network 20 to 80 ms in region, SFU under 5 ms, jitter buffer 20 to 80 ms, decode and render about 20 ms: roughly 100 to 200 ms one way, which is why every hop and every buffer is counted. ITU-T G.114 says most applications are not significantly affected below 150 ms one way, and that 400 ms should not be exceeded for network planning (ITU-T G.114).

Why it's built this way

  1. Conversation breaks down past roughly 150 ms of one-way delay, so media goes over UDP, with no head-of-line blocking, and late packets are dropped.
  2. Home uplinks are small and asymmetric, so each participant uploads once and a server fans out, which rules out a mesh beyond a handful of people.
  3. Decoding and re-encoding cost CPU and add delay, so the server forwards packets and clients compose their own layouts.
  4. Subscribers' links differ by 100x in the same meeting, so publishers send layers and the SFU picks one per subscriber.
  5. Speed of light across continents eats the budget, so participants attach locally and SFUs cascade each stream once between regions.

Failure modes

In the wild

Trade-offs

OptionStrengthsWeaknessesChoose when
Mesh (peer to peer)No server media cost, lowest latency, natural E2EEUplink grows with n, fails past 3 or 4One-to-one and tiny calls
MCU (mix)One stream down, works on weak clientsHeavy server CPU, added delay, fixed layoutPhone dial-in, legacy room systems, recording
SFU (forward)Cheap per stream, client-side layouts, per-subscriber qualityDownlink grows with visible tilesDefault for group meetings
SimulcastWorks with any codec, independent layersMore uplink and encoder CPUBroad device support
SVCOne stream, fine-grained stepsNeeds VP9/AV1 SVC supportControlled clients and newer codecs
UDP mediaNo head-of-line blocking, low delayBlocked on some networksAlways, with a fallback
TCP/TLS 443 fallbackGets through firewallsRetransmits add latency spikesOnly when UDP fails

Leadership lens

Check yourself

  1. Why does the design choose an SFU over a mesh or an MCU for group meetings?

    Show answer

    In a mesh each person uploads n minus 1 streams, so six people at 1 Mbit/s need 5 Mbit/s of upload each, which home links cannot sustain. An MCU pays a full decode and encode per meeting and adds 50 to 200 ms of delay, while an SFU just forwards packets and lets clients compose their own layouts.

  2. A subscriber's estimated downlink drops sharply. How does the SFU decide what that subscriber still receives?

    Show answer

    It allocates the budget across visible tiles in priority order, giving every visible tile its lowest layer first and then upgrading active speaker and pinned tiles one step at a time, with off-screen tiles getting nothing. Video degrades before audio, and hysteresis moves layers down at once but up only after the higher budget has held for a few seconds.

  3. Why is an SFU host's capacity about 5 Gbit/s, and roughly how many hosts does peak egress need?

    Show answer

    Packet rate, not bandwidth, is the limit: 5 Gbit/s of 1,200-byte packets is about 520k packets a second, under 2 microseconds per packet on one core. Peak egress of about 37 Tbit/s divided by 5 Gbit/s is about 7,400 hosts, so about 15,000 across 20 or more regions with failover headroom.

  4. An SFU host crashes mid-meeting. How do its participants recover, and why is there nothing to restore from disk?

    Show answer

    The controller notices through missed heartbeats or client reports, assigns a new SFU, and clients do an ICE restart and re-publish, with a target under 5 seconds. Roster and forwarding state live only in memory and are rebuilt from the clients, so the SFU needs no database.

Sources and further study

Primary

Systems docs

Books

Other curricula

Watch

Free resources

Open in the interactive app: practice, quiz, mock interview →