People hear “build Zoom” and reach for the WebRTC tutorial: two peers, an SDP exchange, media flows directly, done. That works for a 1:1 call and detonates the moment a third person joins, and it is not even in the same universe as a 1000-person all-hands. The core problem in video conferencing is not “how do two browsers connect” - that is solved by WebRTC and I have written about it elsewhere. The core problem is fan-out of live media in a room of N participants under a hard sub-200ms latency budget, and the topology you pick to solve it - mesh, MCU, or SFU - determines whether the product exists at all.

In a call of N people, everyone produces one media stream (their camera + mic) and everyone wants to see the others. That is N producers and up to N x (N-1) stream deliveries. At N = 3 it is trivial. At N = 50 a naive mesh asks every laptop to encode and upload 49 copies of its video, which no consumer uplink survives. At N = 1000 the arithmetic is absurd unless you change the topology entirely. On top of that raw fan-out sit the features that make it Zoom and not a toy: screen share (a second, high-resolution, low-framerate stream), cloud recording (compositing N streams into one MP4 server-side), and breakout rooms (splitting one room into K independent sub-rooms and merging back). And it all has to feel real-time - if the audio lags the lips by more than ~200ms, the call is unusable.

Let me do it properly.

Functional Requirements (FR)

In scope:

  • Multi-party video/audio calls. A meeting with up to 1000 participants. Each participant publishes their own camera + microphone stream and receives the streams of the others (or the relevant subset). Media is real-time, bidirectional, and low latency.
  • Join/leave a meeting. A participant joins via a meeting ID (and optional passcode), gets placed in the room, starts sending/receiving media, and can leave cleanly; the room persists until the host ends it or it empties.
  • Screen share. A participant shares their screen or an application window as an additional high-resolution stream, distinct from their camera. Only one (or a small number of) active screen share(s) at a time. When someone shares, that stream is prioritized to all viewers.
  • Cloud recording. The host can record a meeting. The server composites the active speaker (or a gallery layout) plus screen share into a single durable MP4 stored in the cloud, available for download afterward.
  • Breakout rooms. The host splits the main room’s participants into K smaller sub-rooms, each an independent media session, then can broadcast to all rooms and pull everyone back to the main room.
  • Active speaker detection and layout. The system identifies who is currently speaking (loudest/most consistent audio energy) so clients can highlight them and, in large meetings, so the server can decide whose video to actually forward.
  • In-meeting controls. Mute/unmute, camera on/off, host controls (mute others, remove, lock meeting). These are signaling operations, not media.

Explicitly out of scope (state this to control scope):

  • Meeting scheduling, calendar integration, and the pre-join lobby UI. These are a CRUD web app in front of the meeting; assume a valid, authorized meeting_id and user_id reach us.
  • Chat/reactions/polls. A separate real-time messaging channel (WebSocket text), not the media path. Mentioned, not deep-dived.
  • Telephony / PSTN dial-in (SIP gateways). Real in Zoom, but a media-gateway bolt-on that transcodes phone audio into the same room; out of scope here.
  • End-to-end encryption mode. Zoom offers an E2EE mode that disables server-side features (recording, cloud transcription). We design the default: DTLS/SRTP hop-by-hop encrypted, server can see media to route/composite it.
  • Full auth/identity stack. Assume authenticated requests carry a resolved user_id.

The decision that drives everything: media does not flow peer-to-peer in a real meeting; it flows through a server that receives each participant’s stream once and forwards it selectively. That server is a Selective Forwarding Unit (SFU), and choosing it over mesh or MCU is the single most important call in the whole design.

Non-Functional Requirements (NFR)

  • Scale: 300M daily meeting participants. Assume ~30M concurrent participants at peak across the globe, spread over ~3-5M concurrent meetings. Meeting size ranges from 2 to 1000; the vast majority are small (2-10), but the long tail (webinars, all-hands) at hundreds to a thousand is where the design earns its keep.
  • Latency: glass-to-glass (camera to remote screen) under ~200ms for interactive calls; audio latency is the strict one because humans notice audio lag before video. Mouth-to-ear must stay under ~200ms end to end, which after network RTT leaves the server tens of milliseconds to receive, route, and forward. This is a real-time-media budget, not a request-response one.
  • Availability: 99.99%+. A meeting dropping mid-sentence is a severe failure. The media plane must degrade gracefully - lose a video layer before you lose audio, and never drop the whole call because one component hiccups.
  • Consistency: media is best-effort and lossy by nature - a dropped video frame is discarded, never retransmitted, because a late frame is useless. Audio and video use UDP/SRTP, not TCP. The only strongly-consistent state is meeting membership and control (who is in the room, who is host, who is muted), which is small and lives in a signaling store. Media itself has no consistency requirement beyond “recent.”
  • Durability: live media is disposable. The one durable artifact is the cloud recording - once a meeting is recorded and finalized, that MP4 must never be lost (object storage with replication). Meeting metadata (occurred, duration, participants) is durable for billing/history.

Back-of-the-Envelope Estimation (BoE)

Let me ground it in numbers. Media estimation is about bitrate, not QPS.

Participants and meetings:

Daily meeting participants:   300,000,000
Peak concurrent participants: ~30,000,000   (assume ~10% online at peak)
Avg meeting size:             ~6 participants
Concurrent meetings at peak:  30M / 6 ≈ 5,000,000 meetings
Large meetings (100-1000):    a small % of meetings, but each is a heavy unit

Per-stream bitrate (the fundamental unit):

Camera video (720p, VP8/VP9/H.264): ~1.5 Mbps  (Zoom uses simulcast layers)
  Simulcast layers per publisher:   ~150 Kbps (low) / 500 Kbps (mid) / 1.5 Mbps (high)
Audio (Opus):                        ~40 Kbps
Screen share (1080p, low fps):       ~2-3 Mbps  (high res, ~5-15 fps)

Ingress (uplink into the SFU) - one copy per publisher:

Each publisher uploads ONE encoded stream (or a few simulcast layers), not N copies.
  30M participants * ~1.5 Mbps (video) + 40 Kbps (audio)
  ≈ 30M * ~1.6 Mbps ≈ 48 Tbps of ingress at peak

Egress (downlink out of the SFU) - the real firehose:

Egress is where fan-out bites. In a meeting of N, the SFU forwards each of the
N streams to (N-1) receivers. Total egress streams per meeting ≈ N * (N-1).

Small meeting (N=6):   6 * 5   = 30 forwarded streams
Large meeting (N=1000): the SFU CANNOT forward 1000 videos to each of 1000 people.
  1000 * 999 ≈ 1,000,000 stream-forwards would be ~1,000,000 * 1.5 Mbps = 1.5 Tbps
  for ONE meeting. Impossible on a client uplink and pointless (no one sees 1000 tiles).

That last line is the whole ballgame: large meetings force the SFU to forward only a subset - the active speaker plus a handful of recently-active/visible participants, not everyone. More on that in the deep dive.

SFU server capacity:

A single SFU media node handles roughly ~1-2 Gbps of forwarding
(CPU-bound on packet processing, SRTP encrypt/decrypt, congestion control), so
call it ~500-1000 forwarded streams per node.
Total egress ~ (much larger than ingress). To carry tens of Tbps of egress:
  tens of thousands of SFU nodes globally, placed at the edge near users.

Recording storage:

1080p composited recording ≈ ~2 Mbps ≈ ~900 MB/hour.
Say 5% of the 300M daily participants are in recorded meetings, avg 1 hour,
and each recorded MEETING (not participant) yields one file:
  ~5M recorded meeting-hours/day * ~900 MB ≈ ~4.5 PB/day of recordings
Recordings are retained (per plan), served via CDN -> object storage, tiered.

Signaling load:

Join/leave/mute/layout are control messages over WebSocket, tiny (~hundreds of bytes),
but at 300M joins/day plus continuous control chatter:
  join rate ≈ 300M / 86,400 ≈ ~3,500 joins/sec average, 10x at peak ≈ 35K/sec
  plus a persistent signaling WebSocket per participant (30M concurrent).

The numbers say three distinct scaling problems: an enormous edge media fan-out plane (SFU fleet, egress-bound, latency-bound), a separate signaling/control plane (30M WebSockets, small messages, strongly-consistent room state), and an offline media-processing plane (recording compositing, petabytes). We design each explicitly, and the media plane is where the interview is won.

High-Level Design (HLD)

Zoom is two planes that must be kept separate: a signaling/control plane (who is in the room, host actions, negotiating connections) that is a fairly ordinary stateful WebSocket service, and a media plane (the actual audio/video packets) that is a specialized, latency-critical, edge-deployed SFU fleet. Signaling is TCP/WebSocket and cares about correctness; media is UDP/SRTP and cares about microseconds. Mixing them is the classic mistake.

   ┌──────────────────────────────────────────────────────────────────┐
   │  Clients (desktop / mobile / web-WebRTC)                          │
   │   - WebSocket to Signaling for join/control/negotiation            │
   │   - UDP/SRTP media to an assigned SFU edge node                    │
   └───────┬───────────────────────────────────────┬───────────────────┘
           │ (A) signaling: WSS                     │ (B) media: UDP/SRTP
           ▼                                        ▼
   ┌────────────────────┐                   ┌────────────────────────────┐
   │  Signaling Service  │  assign meeting   │   SFU Media Edge Fleet      │
   │  (stateful WS)      │──to SFU node─────▶│   (per-region, per-meeting  │
   │  - room membership  │◀──ICE/SDP────────│   media routing; selective   │
   │  - host/control     │                   │   forwarding, simulcast)     │
   │  - active speaker   │                   └───────┬────────────┬────────┘
   └───────┬─────────────┘                           │            │
           │ room state                     media in │            │ media
           ▼                                 (large mtg cascade)   ▼
   ┌────────────────────┐                   ┌─────────────┐  ┌──────────────┐
   │  Meeting Metadata   │                   │ SFU <-> SFU  │  │ Recording/    │
   │  Store (SQL: rooms, │                   │ cascade mesh │  │ Composite     │
   │  participants,      │                   │ (relay btwn  │  │ workers ->    │
   │  host, config)      │                   │  regions)    │  │ Object Store  │
   └────────────────────┘                   └─────────────┘  │  + CDN         │
           │                                                  └──────────────┘
           ▼
   ┌────────────────────┐   Global directory:
   │  Meeting Directory  │   meeting_id -> home region / coordinating SFU
   │  (Redis): meeting-> │   participant -> assigned SFU node
   │  region, SFU nodes  │
   └────────────────────┘

Request flow - joining a meeting and getting media (the hot path):

  1. Client opens a signaling WebSocket and sends JOIN {meeting_id, passcode}. Signaling authorizes against meeting metadata (valid meeting, allowed user, not locked).
  2. Signaling looks up the meeting in the meeting directory (Redis): which region/SFU is coordinating this meeting? If the meeting has no home yet, assign one - an SFU node in the region closest to the host/first participants. Return the assigned SFU edge node for this client (usually the one nearest the client geographically, which may cascade to the meeting’s home SFU for large/multi-region meetings).
  3. Client and SFU do a WebRTC negotiation over signaling: exchange SDP offer/answer and ICE candidates (STUN for reflexive addresses, TURN as a UDP-blocked fallback). This establishes a DTLS/SRTP session between the client and its SFU node.
  4. Client publishes its camera+mic (as simulcast layers) up to the SFU over UDP/SRTP. The SFU receives each publisher’s stream exactly once.
  5. The SFU selectively forwards streams down to each subscriber: for a small meeting, everyone’s stream to everyone; for a large meeting, only the active speaker + a bounded set of visible participants, at the layer (resolution) each subscriber’s bandwidth can handle.
  6. Signaling continuously runs active-speaker detection (from audio energy the SFU reports) and pushes layout/speaker updates to clients over the WebSocket so the UI knows whom to spotlight and the SFU knows whose high-res layer to forward.
  7. In parallel, if recording is on, a recording worker subscribes to the meeting’s streams like a silent participant, composites them, and writes an MP4 to object storage.

The key split: signaling (WebSocket, TCP, strongly-consistent room state, tolerant of ~100ms) is completely separate from media (UDP/SRTP, best-effort, sub-200ms, edge-deployed). Signaling tells clients where to send media and who is in the room; the SFU moves the actual bits. Each scales and fails on its own terms.

Component Deep Dive

Four hard parts: (1) the media topology - mesh vs MCU vs SFU, the decision that makes 1000-party calls possible; (2) selective forwarding at scale - how one SFU serves a 1000-person room without forwarding a million streams; (3) geo-distribution and SFU cascading - keeping latency sub-200ms globally and spanning regions; and (4) recording and breakout rooms - the server-side compositing and room-splitting that ride on top. Each goes naive -> where it breaks -> the fix.

1. Media Topology: Mesh vs MCU vs SFU

This is the spine. In a meeting of N, how does everyone’s media reach everyone?

Approach A: Mesh (full peer-to-peer, every pair connected directly)

Each participant opens a direct WebRTC connection to every other participant and sends their encoded stream to each one. No media server at all.

join(meeting):
    for peer in participants:
        connect_p2p(me, peer)
        send_my_stream(peer)      # <-- I encode and upload N-1 copies of myself

Beautiful for 1:1: lowest latency, no server media cost, naturally E2E encrypted. Where it breaks: uplink and CPU explode as O(N). Each participant must encode and upload their own video N-1 times (one per peer), because each peer connection is independent. At N=5 a participant uploads 4 copies of their 1.5 Mbps video = 6 Mbps up, which a home connection barely tolerates. At N=10 it is 13.5 Mbps up per person, and consumer uplinks die. CPU for N-1 simultaneous encodes melts a laptop. Mesh is a dead end past ~4-6 people; it cannot approach 1000.

Approach B: MCU (Multipoint Control Unit - server mixes everything into one stream)

A central server receives every participant’s stream, decodes them all, composites them into a single mixed video (a grid) and mixed audio, re-encodes that one combined stream, and sends each participant a single downstream. Each client uploads one stream and downloads one stream - trivial for the client.

server:
    decode(all N streams)
    composite -> one grid frame; mix -> one audio track
    encode once
    send the single mixed stream to each participant

Great for weak clients (one up, one down, fixed bitrate regardless of N). Where it breaks: the server cost is brutal and the quality/flexibility is bad. Decoding N streams, compositing, and re-encoding per meeting is enormously CPU-heavy - transcoding is the most expensive thing you can do in media, and doing it in real time for millions of meetings needs a fortune in hardware. It also adds encode/decode latency (blowing the 200ms budget) and forces one layout on everyone - you cannot let each client choose who to spotlight, pin, or hide, because they all get the same pre-composited frame. MCU is why old hardware video-bridges were expensive and rigid. Not viable at Zoom’s scale or flexibility needs.

Approach C: SFU (Selective Forwarding Unit - the design Zoom actually uses)

The SFU is the middle ground and the correct answer. Each participant uploads their stream once to the SFU. The SFU does not decode or re-encode - it just forwards the received packets to the other participants, choosing which streams and which quality layer to send to each subscriber.

publisher -> SFU:  upload MY stream once (as simulcast layers)
SFU -> subscriber: forward the streams this subscriber should see,
                   each at a layer that fits their bandwidth
                   NO transcoding - just route packets

Why it wins:

  • Client uplink is O(1). Each participant uploads exactly one stream (a few simulcast layers of themselves) regardless of N. This is what breaks the mesh uplink wall and makes large meetings feasible.
  • Server does no transcoding. Forwarding packets is cheap compared to decode/encode. The SFU is bandwidth-and-packet-bound, not compute-bound, so one node handles far more participants than an MCU could.
  • Flexible layouts. Because the SFU forwards individual streams (not a pre-mixed grid), each client composites its own view locally and can pin, spotlight, or hide anyone. The server just decides which streams to send.
  • Simulcast makes bandwidth adaptation free. Each publisher encodes its camera at multiple resolutions/bitrates simultaneously (e.g., 1.5 Mbps / 500 Kbps / 150 Kbps). The SFU picks, per subscriber, which layer to forward based on that subscriber’s measured downlink and how prominently they are displaying that stream (spotlight = high layer, tiny gallery tile = low layer). No transcoding needed - the layers already exist.
Publisher A encodes simulcast:  [high 1.5M] [mid 500K] [low 150K]
                                       │        │         │
SFU forwards per-subscriber:           │        │         └─▶ B (weak wifi, tiny tile)
                                       │        └───────────▶ C (mid, gallery)
                                       └────────────────────▶ D (spotlighting A, full res)
   One upload from A -> three different qualities out, zero transcoding.

Where the SFU still needs help: egress. Forwarding is cheap per stream, but in a 1000-person room the SFU still cannot forward 1000 streams to each of 1000 people (that is the 1.5 Tbps-per-meeting number from the BoE). That is not a topology problem - it is a selection problem, solved next.

Verdict: SFU, always, for real meetings. Mesh only for the smallest calls (and even those often route through an SFU for consistency and recording). MCU only where a client is too weak for multiple streams (legacy/telephony gateways) - and even then it is a bolt-on, not the core.

2. Selective Forwarding at Scale: A 1000-Person Room

The SFU solves uplink. Egress in huge meetings is the next wall.

Naive: SFU forwards every participant’s stream to every other participant

For a room of N, forward all N streams to all N subscribers.

for each subscriber S:
    for each publisher P (P != S):
        forward(P.stream, S)     # N*(N-1) forwards

Where it breaks: at N=1000 this is ~1,000,000 stream-forwards per meeting, ~1.5 Tbps for one meeting, on subscriber downlinks that top out at a few Mbps. It is also pointless: no human watches 1000 video tiles, and a screen showing 1000 people renders each as a 20-pixel thumbnail. Forwarding everything is both impossible and useless.

Better: forward only what is seen and heard - active-speaker + bounded visible set

Humans in a big meeting look at one speaker and maybe a handful of others. Forward accordingly.

  • Audio: forward only the top few loudest streams. The SFU does lightweight audio-energy detection (reading the audio level RTP header extension - no decoding) on all incoming audio and forwards only the top ~3 loudest speakers to everyone. Mixing 1000 audio tracks is meaningless (it is just noise); the active-speaker set is what matters. Everyone else’s audio is dropped at the SFU until they become loud enough to enter the top set. This turns audio egress from O(N) to O(1) per subscriber.
  • Video: forward only the visible subset at the layer that fits the tile. The client tells the SFU which streams it is actually displaying (the spotlight speaker at high res, plus the ~5-25 gallery tiles currently on screen at low res). The SFU forwards only those, each at the appropriate simulcast layer. A participant scrolled off-screen gets their video paused entirely (the SFU stops forwarding it and tells the publisher to stop sending that layer). Video egress per subscriber drops from N streams to a bounded ~25.
  • Active-speaker detection drives the spotlight. The SFU aggregates audio energy over a short window, picks the dominant speaker, and pushes a “current speaker = X” event through signaling. Clients spotlight X and the SFU promotes X’s video to the high layer for everyone viewing the spotlight. This is the mechanism that keeps a 1000-person webinar to a few streams of actual egress per viewer instead of a thousand.
  • Publisher-side pausing closes the loop. If no one is subscribed to a publisher’s high layer (nobody is spotlighting them), the SFU signals that publisher to stop encoding/sending the high layer entirely (via RTCP or a signaling hint). This saves ingress and the publisher’s uplink/CPU too - you do not upload 1.5 Mbps that no one watches.
Room N=1000. Subscriber S sees: 1 spotlight (high) + 24 gallery tiles (low) + top-3 audio.
  Video egress to S: 1*1.5M + 24*150K ≈ 5.1 Mbps  (not 1000*1.5M = 1.5 Gbps)
  Audio egress to S: 3*40K ≈ 120 Kbps
  Fits a normal broadband downlink. The other 975 streams are simply not sent to S.

Verdict: never forward everything. Forward the top-K loudest audio (K~3) and the client-declared visible video subset, each at the simulcast layer that fits the tile and the downlink, and pause publishers no one watches. Selection is what makes N=1000 fit inside a home connection.

3. Geo-Distribution and SFU Cascading

Sub-200ms glass-to-glass is a physics problem. A participant in Tokyo and one in London cannot both route through a single SFU in Virginia without blowing the latency budget on network RTT alone.

Naive: one SFU per meeting, everyone connects to it

Assign each meeting a single SFU node; all participants send/receive media there.

Where it breaks: the speed of light. Tokyo-to-Virginia RTT is ~150ms one way; route a Tokyo participant’s audio through Virginia to another Tokyo participant and it travels the Pacific twice - well over 200ms mouth-to-ear before the server does anything. A single home SFU also caps meeting size at one node’s capacity (~1000 streams), and it is a single point of failure for the meeting. Global meetings and huge meetings both break.

Better: edge SFUs per region, cascaded into a distribution tree

  • Each participant connects to the SFU edge nearest them. A Tokyo user hits a Tokyo SFU; a London user hits a London SFU. Client-to-SFU RTT stays tiny (single-digit to low-tens of ms), preserving the budget.
  • SFUs cascade: they relay between each other, not through clients. The regional SFUs form a distribution tree/mesh for the meeting. The Tokyo SFU forwards the aggregate of its local publishers once to the London SFU, which fans it out to its local subscribers. So a stream crosses each inter-region link once, not once per remote subscriber. This is “cascading SFUs” and it is how you get a globally distributed meeting under budget: local fan-out is local, and the expensive long-haul link carries one copy per stream, not one per viewer.
Tokyo users ─▶ [Tokyo SFU] ═══one relay per stream═══▶ [London SFU] ─▶ London users
                    ▲                                        │
                    └══════════ [Virginia SFU] ◀═════════════┘
   Each edge does local fan-out. Inter-region links carry 1 copy per active stream,
   selected (active-speaker) so the trunk stays thin even for a 1000-person global call.
  • A coordinating/home SFU per meeting owns the routing state. One node (in the meeting’s home region, chosen at creation from where the host/first joiners are) is the authority for the meeting’s participant list and the cascade topology. Edge SFUs register with it. The meeting directory (Redis) maps meeting_id -> home region + participating SFU nodes so signaling can place new joiners on the right edge and wire them into the cascade.
  • Capacity beyond one node. Because the meeting spans multiple SFU nodes, a 1000-person meeting is not capped by a single node’s ~1000-stream limit - participants are spread across regional edges, each handling its local subset, cascading the selected streams between them. Horizontal scale of the meeting itself.
  • Failure handling. If an edge SFU dies, its participants’ clients detect media stall (RTCP timeout) and re-signal to a sibling SFU in the same region, re-negotiate ICE, and rejoin the cascade - a few seconds of reconnection, not a dropped meeting. The home SFU’s state is replicated so a home failure promotes a standby. Media loss during the gap is just discarded frames; nothing to recover because media is disposable.

Verdict: connect each participant to their nearest edge SFU, cascade selected streams between regional SFUs so long-haul links carry one copy per active stream, coordinate through a per-meeting home SFU tracked in a directory, and reconnect to a sibling on node failure. This is what holds sub-200ms for a global, thousand-person meeting.

4. Recording and Breakout Rooms

These ride on top of the SFU as specialized subscribers and control operations.

Cloud recording - naive: client uploads its local recording

Have the host’s client record the composited view it renders and upload the file afterward.

Where it breaks: the client only has the streams it received (selected, low-layer for gallery tiles), its recording quality tracks its own bandwidth and layout, it dies if the host’s laptop closes, and it cannot capture what the host never saw. Client-side recording is unreliable and low-fidelity.

Recording - better: a server-side recording worker as a silent participant

  • A recording worker joins the meeting like a participant (subscribing to the SFU), but it subscribes to the high layers of all relevant streams (active speaker + screen share, or the full gallery for grid recordings). Because it is server-side and on the data-center backbone, it is not bandwidth-constrained the way a client is.
  • It composites and encodes to a durable MP4. Unlike the live path (which never transcodes), recording does decode the selected streams, composite them into the chosen layout (active-speaker with screen share is the common one), mux with mixed audio, and encode a single H.264/AAC MP4. This is expensive transcoding, but it is offline-tolerant - it does not sit in the 200ms live budget, so it runs on a separate pool of compute-heavy workers that can lag slightly behind real time.
  • Written to object storage, served via CDN. The finalized MP4 lands in replicated object storage (durable - this is the one artifact we must not lose), with a metadata row (owner, meeting, duration, size, storage key). Playback and download go through the CDN. Recording storage is tiered (hot recent, cold archive) given the ~PB/day volume.
  • Screen share is just another stream to the SFU and the recorder. It is a separate high-resolution, low-framerate publisher (the sharer publishes their screen as an additional track). The SFU forwards it prioritized to all viewers (screen share always gets a good layer because it is the focus), and the recorder composites it as the main frame with the speaker inset.

Breakout rooms - splitting one room into K

  • Breakout rooms are K independent meeting sessions with a shared parent. When the host creates breakouts, the signaling service creates K new logical rooms (child meeting_ids under the parent) and reassigns participants: each assigned participant tears down its subscription to the main room’s streams and re-subscribes within its breakout room (possibly on the same SFU nodes, just a different room grouping). Mechanically it is “leave main room, join breakout room” driven by the host, not the user.
  • The parent room persists. The host retains control of the parent and can broadcast a message to all breakouts (a signaling push to every child room) and close breakouts (reassign everyone back to the parent room and re-subscribe). Because rooms are just membership groupings over the same SFU fleet, split and merge are signaling operations plus media re-subscription, not new infrastructure.
  • Isolation. Media in a breakout room is forwarded only within that room’s participant set - the SFU scopes forwarding by room membership, so breakout A never leaks into breakout B even if they share a physical SFU node.

Verdict: recording is a server-side silent subscriber that transcodes offline into durable, CDN-served MP4s (the only place we transcode and the only durable media); screen share is a prioritized extra stream; breakout rooms are K child rooms over the same SFU fleet, split and merged by signaling with media re-subscription. All three are features layered on the SFU primitive, not separate systems.

API Design & Data Schema

Signaling API (WebSocket messages, client <-> signaling service)

WSS /v1/signal                                  # persistent signaling socket

client -> server:
  JOIN     { meeting_id, passcode?, name }
  OFFER    { sdp }                              # WebRTC SDP offer to the SFU
  ICE      { candidate }                        # trickle ICE candidates
  PUBLISH  { kind: "camera"|"screen", simulcast:[layers] }
  SUBSCRIBE{ stream_ids:[...] }                 # which streams I'm displaying (drives selection)
  CONTROL  { action: "mute"|"unmute"|"cam_off"|"leave" }
  HOST     { action:"mute_all"|"remove"|"lock"|"record_start"|
             "breakout_create"|"breakout_close", params }

server -> client:
  JOINED       { self_id, sfu_node: {ip, port, ice}, participants:[...] }
  ANSWER       { sdp }                          # SFU's SDP answer
  ICE          { candidate }
  PARTICIPANT  { event:"join"|"leave", user_id, streams }
  ACTIVE_SPEAKER { user_id }                    # drives spotlight + high-layer forward
  LAYOUT       { spotlight, gallery:[...] }
  ERROR        { code, reason }                 # meeting_full, locked, unauthorized

Control-plane REST API (meeting lifecycle, out-of-band of media)

POST /v1/meetings                               # create/schedule a meeting
  Body: { host_id, scheduled_at?, settings:{max_participants,record,passcode} }
  201: { meeting_id, passcode, join_url }

GET  /v1/meetings/{meeting_id}                   # metadata / status
  200: { status:"scheduled"|"live"|"ended", participant_count, home_region }

POST /v1/meetings/{meeting_id}/end               # host ends meeting

GET  /v1/recordings/{meeting_id}                 # list recordings
  200: { recordings:[ {file_id, duration, size, url(cdn)} ] }

Design notes:

  • Signaling is WebSocket; media is a separate UDP/SRTP transport negotiated via that signaling (SDP offer/answer + trickle ICE). The REST API is only for lifecycle (create, end, fetch recordings) and never touches media.
  • SUBSCRIBE carries the client’s visible set - this is what lets the SFU forward only what is displayed. It is updated continuously as the user scrolls the gallery or pins someone.
  • HOST actions are authorized against room state (only the host role can mute-all or create breakouts), enforced in the signaling service which owns the strongly-consistent membership.

Data schemas

Meeting + participant state (SQL) - the strongly-consistent control state:

meetings
  meeting_id     BIGINT PK
  host_id        BIGINT
  status         ENUM        -- scheduled | live | ended
  home_region    TEXT        -- coordinating SFU region
  max_participants INT
  record         BOOL
  passcode_hash  TEXT
  created_at     TIMESTAMP
  ended_at       TIMESTAMP NULL
  INDEX (host_id), INDEX (status)

participants                  -- live membership, source of truth for authz
  meeting_id     BIGINT
  user_id        BIGINT
  role           ENUM         -- host | cohost | attendee
  sfu_node_id    TEXT         -- which edge SFU this participant is on
  breakout_room  BIGINT NULL  -- child room id, NULL = main room
  muted          BOOL
  joined_at      TIMESTAMP
  PRIMARY KEY (meeting_id, user_id)
  INDEX (meeting_id)          -- enumerate a room's members / breakout grouping

Why SQL here: membership and control state is small, relational, correctness-critical (host authz, who is muted, who is in which breakout), read on every control action. It is a natural relational fit, heavily cached in Redis. This is the only strongly-consistent state; it is tiny compared to media.

Meeting directory + routing (Redis) - the fast live index:

KEY  meeting:{meeting_id}       -> { home_region, sfu_nodes:[...], participant_count }
KEY  participant:{user_id}      -> { meeting_id, sfu_node_id, ttl }
KEY  sfu:{node_id}:load         -> current stream count / bandwidth (for placement)
   Used by signaling to place joiners on the least-loaded nearby SFU and wire cascade.

Why NoSQL/in-memory here: this is ephemeral, high-churn routing state read on every join and every reconnect, needs sub-millisecond lookups, and does not need durability (it is rebuildable from live sessions). Redis, not SQL.

Recording metadata (SQL) + files (object storage):

recordings
  file_id       UUID PK
  meeting_id    BIGINT INDEX
  owner_id      BIGINT
  duration_sec  INT
  size_bytes    BIGINT
  storage_key   TEXT           -- object-store path
  layout        ENUM           -- speaker | gallery | screenshare
  created_at    TIMESTAMP
  -- bytes live in replicated object storage, served via CDN, tiered hot/cold

Media itself has no schema - it is SRTP packet flows, disposable, never stored except as the composited recording. That is the point: the durable footprint of a video system is tiny (control state + finalized recordings); the expensive part is moving disposable bits fast.

Bottlenecks & Scaling

Where it breaks first, in order, and the fix for each:

1. Client uplink in a mesh (breaks first, at N~5). Every participant encoding and uploading N-1 copies of themselves. Fix: SFU topology - each client uploads exactly one stream (simulcast layers) to the server regardless of N. This is the foundational fix; without it there is no multi-party product.

2. SFU egress in large meetings. Forwarding N streams to N subscribers is O(N^2) and impossible past a few hundred. Fix: selective forwarding - top-K (~3) loudest audio to everyone, only the client-declared visible video subset, each at the simulcast layer that fits the tile and downlink, plus pause publishers no one watches. Egress per subscriber becomes a bounded ~5 Mbps, not O(N).

3. Latency budget on global/large meetings. One home SFU forces long-haul round trips that blow 200ms. Fix: edge SFUs per region + cascading. Each participant hits their nearest edge; SFUs relay selected streams to each other so inter-region trunks carry one copy per active stream, not one per viewer. Local fan-out stays local.

Shard/placement key choice: the meeting is the unit. Meetings are placed by meeting_id onto a home region + SFU set, and participants are assigned to the nearest edge SFU and grouped by (meeting_id, breakout_room) for forwarding scope. Sharding by user would split a meeting’s forwarding across unrelated nodes; keeping a meeting’s routing coordinated by its home SFU keeps membership and cascade state coherent.

4. Single SFU node as a capacity wall and SPOF. One node caps at ~1000 streams and its death drops its participants. Fix: spread a large meeting across multiple regional SFUs (the cascade already does this), run standby home SFUs with replicated meeting state, and on edge-node death have clients reconnect to a sibling SFU via re-signaling (a few seconds, media gap is just discarded frames).

5. Recording transcode cost. Compositing and encoding N streams per recorded meeting is heavy CPU. Fix: offline recording workers on a separate compute-heavy pool, decoupled from the live 200ms path, allowed to lag slightly. Output to durable replicated object storage, CDN-served, tiered hot/cold given ~PB/day volume. Recording is the only place we transcode.

6. Hot meeting = hot SFU node. A 1000-person webinar concentrates load. Fix: the cascade spreads it across edges; within a region, place participants on the least-loaded SFU (tracked in Redis sfu:{node}:load); for a webinar (one-to-many, few speakers), use a broadcast tree of SFUs where each SFU fans out to a subtree, so no single node fans out to thousands. Screen share and active-speaker selection keep the actual forwarded set tiny.

7. Signaling WebSocket fleet at 30M concurrent. Control sockets and join storms. Fix: a horizontally scaled stateful signaling fleet (like any WebSocket gateway - dozens to hundreds of nodes), room state in Redis + SQL, and jittered reconnect on node death. Signaling messages are tiny, so this scales far more easily than the media plane.

8. TURN relay load for UDP-blocked clients. Corporate firewalls block UDP, forcing media through TURN relays. Fix: a fleet of TURN servers as the fallback path (media relayed over TURN, often TCP/443 to punch through firewalls), placed at the edge near clients; most clients connect UDP directly to the SFU and only the blocked minority pay the TURN hop. Capacity-planned separately since relayed media is pure bandwidth passthrough.

9. Active-speaker thrash and layout churn. Rapid speaker switching could flap high-layer forwarding and spam layout updates. Fix: hysteresis - require a speaker to hold dominance for a short window before promoting their high layer and pushing a spotlight change, and coalesce layout events. Cheap to compute (audio-level header, no decode), but must be damped so the view is stable.

Wrap-Up

The trade-offs that define this design:

  • SFU over mesh and MCU - the one decision that matters most. Mesh dies on client uplink past ~5 people; MCU dies on server transcode cost and forces one rigid layout. The SFU uploads each stream once and forwards packets without transcoding, giving O(1) client uplink, cheap server routing, and per-client flexible layouts. Everything else is built on this.
  • Never forward everything - select ruthlessly. A 1000-person room is only possible because the SFU forwards the top-3 loudest audio and the client’s ~25 visible video tiles at fitted simulcast layers, pausing publishers no one watches. Fan-out is bounded by what humans actually see and hear, not by N.
  • Two planes, kept strictly separate. Signaling (WebSocket, TCP, strongly-consistent room state, ~100ms tolerant) tells clients where to send media and who is in the room; the media plane (UDP/SRTP, best-effort, sub-200ms, edge-deployed SFUs) moves the bits. Mixing a lossy real-time media budget with a correctness-critical control channel is the classic failure.
  • Edge + cascade to beat physics. Each participant hits their nearest SFU; SFUs relay selected streams between regions so long-haul links carry one copy per active stream. Sub-200ms globally is a routing-topology problem, solved by keeping fan-out local and trunks thin.
  • Media is disposable; only recordings are durable. Live audio/video is discarded frame by frame (a late frame is useless, so UDP not TCP, no retransmit). The single durable artifact is the offline-composited recording, transcoded on a separate pool off the live path and stored in replicated, CDN-served object storage.

One-line summary: route every participant’s single uploaded stream through a fleet of edge SFUs that selectively forward only the active-speaker and visible-tile streams (at fitted simulcast layers) and cascade selected streams between regions, keep meeting membership and control on a separate strongly-consistent signaling plane, and push recording and layout compositing to offline workers - so 2-person calls, 1000-person webinars, screen share, breakout rooms, and cloud recording all hold under 200ms at 300M daily participants.