WebRTC Through CDN Infrastructure: Real-Time Video Conferencing at Scale

Last year we pushed a 40,000-participant all-hands through what we thought was a bulletproof CDN setup and watched the whole thing fall apart in under 90 seconds. The SFU in us-east-1 choked, TURN allocations spiked to 12k per second, and half the European participants froze on a single keyframe. It wasn't a codec issue or a client bug — we'd treated WebRTC like just another HTTP stream and tried to force real-time UDP media through infrastructure built for cacheable TCP delivery. That failure taught us more about WebRTC CDN integration than the previous five years of lab tests combined.

Why Traditional CDN Caching Breaks WebRTC's Real-Time Promise

If you've spent any time tuning a CDN for video, your instincts are all about cache-hit ratios, origin shielding, and chunked delivery. None of that applies here. WebRTC doesn't want your edge cache — it wants a low-latency relay path that preserves UDP semantics and DTLS-SRTP encryption end-to-end. Standard CDN PoPs are optimized for TCP, TLS termination, and storing bytes. WebRTC's media plane runs over SRTP/UDP with ICE connectivity checks that expect a direct, stateful path. When we first tried to front our Janus cluster with a regular reverse proxy, the proxy happily terminated DTLS and then had no idea what to do with the SRTP packets that followed.

What actually works is rethinking the edge as a media relay fabric, not a cache tier. Instead of pulling segments from origin, your PoPs need to act as distributed TURN/SFU relays that forward packets without trying to store or re-encode them. I've found the most reliable pattern is to deploy lightweight WebRTC-aware relays at the edge that speak ICE properly and can forward via Anycast, while leaving the heavy Selective Forwarding Unit (SFU) logic to regional media clusters. You keep HTTP for signaling — offer/answer, trickle ICE over WHIP/WHEP or your own WebSocket service — but the media itself never touches the HTTP cache layer. For large broadcasts where you need to fall back to segmented delivery for passive viewers, that's where a parallel Low-Latency HLS Configuration for CDN: Sub-3-Second Live Streaming path makes sense, but don't mix those concerns in the same PoP process.

The signaling plane still benefits from classic CDN optimizations, and this is where the HTTP Working Group Specifications around HTTP/3 and QUIC are worth reading closely. We moved our signaling and WHIP endpoints to HTTP/3 on the edge, which cut connection migration failures for mobile users switching from Wi-Fi to 5G by nearly 40%.

// Edge ICE configuration - Anycast TURN deployed at CDN PoPs
// Don't hardcode a single TURN server; advertise the nearest PoP
const iceConfig = {
  iceServers: [
    {
      urls: [
        "turn:turn-anycast.example-cdn.net:3478?transport=udp",
        "turn:turn-anycast.example-cdn.net:3478?transport=tcp",
        "turns:turn-anycast.example-cdn.net:5349?transport=tcp"
      ],
      username: "webrtc-user",
      credential: "ephemeral-token-from-edge-auth"
    }
  ],
  iceTransportPolicy: "all",
  bundlePolicy: "max-bundle",
  rtcpMuxPolicy: "require",
  // Critical for CDN-hopped media: aggressive nomination
  iceCandidatePoolSize: 2
};

// Client fetches config from edge-auth worker (validated at PoP)
// GET https://edge.example-cdn.net/v1/ice-config
// Response includes short-lived credentials generated at the edge
// via HMAC with daily rotation - no origin round-trip needed

Architecting SFU Clusters at the Edge: Cascading Without Melting Your Network

A centralized SFU works great until you cross about 150 participants. After that, you're burning origin bandwidth and hauling every media stream back to one region. The shift we made in 2021 was to push SFU selectivity to the edge — but not to *every* PoP. Running a full SFU in 300+ PoPs is a nightmare for synchronization and cost. What we've settled on is a two-tier model: ultra-lightweight forwarding relays in all PoPs (essentially smart TURN servers that understand RTP header extensions) and full SFU clusters in 8-12 regional media hubs. The relays get you last-mile latency wins; the regional SFUs handle simulcast layer selection, bandwidth estimation, and simulcast switching.

Cascading is where most teams get this wrong. A naive full-mesh where every SFU forwards every participant's streams to every other SFU will saturate your inter-PoP links within minutes. We learned to implement selective cascading based on actual viewport demand. If no one in eu-west is subscribing to a presenter in ap-south, don't forward those three simulcast layers across the Atlantic. We use a pub/sub control plane (Redis Streams with local PoP agents) to advertise who is subscribed to what, and only then do we establish SRTP forwarding between SFUs. It adds about 15ms of signaling delay on first subscription, but it saves us terabytes per hour during peak. Insights from web.dev Articles on WebRTC bandwidth estimation and CMAF Packaging for CDN Delivery: Unified Streaming Format for HLS and DASH helped us align our simulcast encodings so the same renditions could be reused if we needed to shunt overflow viewers to a low-latency segmented fallback without re-transcoding.

# Regional SFU cascading control - pseudocode for edge orchestrator
# Runs in each regional SFU hub, subscribes to viewport demand events

on_subscription_request(subscriber_region, publisher_id, desired_layers):
  if publisher_id not in local_publishers:
    # Check global registry - which hub owns this publisher?
    owner_hub = global_registry.lookup(publisher_id)
    
    if not has_forwarding_link(owner_hub):
      # Establish SRTP forwarding only on demand
      srtp_link = create_srtp_forward(
        from_hub = owner_hub,
        to_hub = self.region,
        layers = negotiate_layers(desired_layers),
        transport = "udp",  # Don't tunnel SRTP over TCP between PoPs
        dscp_marking = "AF41" # Prioritize media between hubs
      )
      monitor_link_loss(srtp_link, threshold=1.5%)

  # Local SFU then selects optimal simulcast layer for subscriber
  return local_sfu.add_subscriber(
    publisher_id = publisher_id,
    max_bitrate = estimate_downlink(subscriber_region)
  )

Measuring What Actually Matters: Latency, Jitter, and Loss Across PoPs

You can't optimize what you don't measure correctly, and vanilla CDN metrics will lie to you about WebRTC quality. Cache hit ratio is irrelevant; you need end-to-end media latency, inter-arrival jitter, and concealed packet loss per subscriber. We instrument every relay and SFU to emit RTCP Receiver Reports and extended reports (XR) every 500ms, then aggregate at the PoP before shipping to ClickHouse. I don't trust client-side getStats() alone — by the time the client reports a problem, the user has already seen a freeze. Edge-observed jitter is your early warning.

Here's what we see in production after moving from a single us-east SFU to the distributed relay + regional SFU model. These are median values for a 5,000-participant webinar with 12 active publishers, measured over a 60-minute session. The difference isn't subtle, and it's why the old "just put a TURN server behind a load balancer" approach doesn't scale. For very large audiences beyond ~10k passive viewers, we actively compare WebRTC delivery against segmented options using the tradeoffs outlined in DASH vs HLS for CDN Streaming: Protocol Comparison and Selection Guide — at that scale, hybrid WebRTC for speakers + LL-HLS for viewers often wins on cost.

Metric (p50 / p95) Centralized SFU (Single Region) CDN-Distributed SFU + Edge Relays Target SLO
End-to-End Media Latency 185ms / 420ms 62ms / 148ms < 150ms p95
Inter-Arrival Jitter 18ms / 45ms 7ms / 16ms < 20ms p95
Concealed Packet Loss (post-FEC/NACK) 1.8% / 4.2% 0.4% / 1.1% < 1.0% p95
Join Time (ICE + DTLS + First Frame) 1120ms / 2850ms 340ms / 890ms < 800ms p95
Inter-PoP Backbone Bandwidth (5k viewers) 38.5 Gbps (full fan-out) 9.2 Gbps (selective cascade) N/A (cost metric)

Putting It into Practice: Deploying TURN and SFU Routing at the Edge

This is where most teams trip up — they try to bolt WebRTC onto their existing CDN PoPs like it's just another HTTP service. It isn't. WebRTC is stateful, UDP-heavy, and incredibly sensitive to NAT traversal. You can't cache it, you have to *route* it. The practical pattern we've settled on after a few painful rollouts is this: anycast your STUN/TURN fleet right alongside your edge SFUs, but keep them as separate scaling units. TURN is pure relay bandwidth, SFU is CPU and packet-forwarding logic. If you co-schedule them tightly, a single 10k-participant webinar will starve your SFU of CPU while your TURN nodes sit idle.

For a global rollout, you'll want at least one TURN/SFU pair per major region (think 6-8 PoPs to start, not 150). Use GeoDNS or anycast to steer clients to the nearest PoP based on ICE RTT, not HTTP ping — we've seen HTTP-based steering add 40-60ms of unnecessary detour for UDP media. Set your ICE timeout aggressively: we use a 3-second ICE gathering timeout and prune candidates after 800ms if we haven't got a relay candidate. Clients that can't establish a peer connection within 4.5 seconds should be failed over to TURN-TCP on port 443 immediately. Don't let them spin. That one tweak cut our join-failure rate from 4.1% to 0.7% across networks in Southeast Asia.

// Edge SFU + TURN config example (mediasoup + coturn)
# turnserver.conf - deploy per PoP via anycast
listening-port=3478
tls-listening-port=5349
listening-ip=0.0.0.0
relay-ip=10.10.1.20  # Anycast edge IP
external-ip=203.0.113.50
lt-cred-mech
user=webrtc:0a8f7e9c2d1b4e6f
realm=edge.example.com
no-tcp-relay  # Force UDP relay first, fallback via separate TCP instance

// Client-side ICE config with cascading SFU selector
const iceServers = [
  { urls: "stun:stun.edge.example.com:3478" },
  { 
    urls: "turn:turn.edge.example.com:3478?transport=udp",
    username: "webrtc",
    credential: "0a8f7e9c2d1b4e6f"
  },
  {
    urls: "turn:turn.edge.example.com:443?transport=tcp",
    username: "webrtc",
    credential: "0a8f7e9c2d1b4e6f" // Fallback for strict firewalls
  }
];

// Select nearest SFU - don't use HTTP latency
async function selectSFU() {
  const candidates = ["us-east.edge.example.com", "eu-west.edge.example.com", "ap-south.edge.example.com"];
  const results = await Promise.all(candidates.map(async host => {
    const start = performance.now();
    await fetch(`https://${host}/healthz`, { method: 'HEAD', cache: 'no-store' });
    // Replace with actual UDP ICE ping in production
    return { host, rtt: performance.now() - start };
  }));
  return results.sort((a,b) => a.rtt - b.rtt)[0].host;
}

Advanced Patterns and Troubleshooting Cascaded SFUs in Production

Once you've got basic SFU clustering working, you'll hit the next wall: simulcast and bandwidth adaptation. Don't try to force every publisher to send a single 2.5 Mbps 1080p stream and have the SFU transcode — you'll melt your edge CPUs. Instead, have publishers send 3 simulcast layers (say, 720p @ 1.2 Mbps, 360p @ 400 kbps, 180p @ 150 kbps) and let the SFU do simple selective forwarding. For larger sessions, layer in SVC with VP9 or AV1; it costs a bit more on the encode side but lets you forward at the temporal/spatial layer without any transcoding. We typically see a 55-65% reduction in egress bandwidth versus full transcoding, and CPU per forwarded stream drops from ~0.8 cores to ~0.05 cores.

When cascades *do* fail — and they will — it's almost always one of three things: inter-SFU link congestion, clock drift causing jitter buffer bloat, or a thundering herd on reconnect. For inter-SFU links, don't use public internet if you can help it. Use your CDN's private backbone and monitor inter-PoP RTT continuously; if RTT between us-east and eu-west spikes above 110ms, we automatically de-preference that cascade path and re-route via us-central. For jitter, keep your SFU's jitter buffer between 30-80ms — anything over 120ms and users will start talking over each other, even if packet loss looks clean. And if you're doing true 1-to-many broadcast (think 5k+ viewers), honestly evaluate whether you need WebRTC at all. For those cases, Low-Latency DASH with CMAF Chunks: Achieving Near-Real-Time CDN Delivery will get you 1.5-2.5 second latency at a fraction of the cost. We often hybridize: WebRTC for the first 50 active speakers, LL-DASH for the passive audience, stitched via our standard CDN Live Streaming Architecture: Ingest, Transcode, Package, and Deliver at Scale.

One last operational gotcha: state. SFUs are stateful, so your usual stateless edge autoscaling will kill sessions on deploy. You need graceful draining — we mark an SFU as "draining" and stop assigning new sessions, then wait up to 90 seconds for existing rooms to migrate via cascade handoff before terminating. Pair that with CDN DVR Functionality at the Edge: Time-Shifted Viewing for Live Streams if you need rewind for late joiners; don't try to buffer that in the SFU itself. Your SFU should only buffer ~2 seconds for retransmits (NACK/PLI handling), everything else belongs in your DVR/packager tier. Alert on these three golden signals: SFU packet-loss-concealment rate >2%, inter-SFU cascade latency p95 >150ms, and ICE failure rate >1.5% per PoP — those catch 90% of user-visible issues before support tickets do.

Frequently Asked Questions

When should we use WebRTC vs. Low-Latency DASH/CMAF for live delivery?

Use WebRTC when you need sub-500ms latency and true interactivity — conferencing, live auctions, collaborative editing where participants talk to each other. If it's primarily one-to-many viewing and 1.5-3 seconds of latency is acceptable, Low-Latency DASH with CMAF is far more scalable and CDN-friendly. It leverages HTTP caching and scales to millions without per-viewer SFU state. Many platforms now mix both: WebRTC for active participants and LL-DASH for the viewing audience.

How do you scale a WebRTC SFU cascade beyond 200 participants in a single room?

Don't put everyone on one SFU. Implement cascading with a selective forwarding topology: designate one SFU per region as the primary for that room, then cascade only the active speaker streams (Last N = 4-6 video tiles + dominant speaker logic). Each edge SFU forwards just those streams to its local participants. This keeps fan-out per SFU under 150-200 subscribers and limits cross-region bandwidth to 6-8 streams instead of N-squared. Expect to add a new SFU shard every ~120 concurrent subscribers if you're forwarding 720p simulcast.

Why are my TURN relay costs exploding?

Usually because 30-40% of your clients are falling back to TURN due to symmetric NATs or UDP-blocking firewalls, and you're relaying high-bitrate streams through TURN instead of just ICE negotiation. Audit your candidate pair stats — if `relay` candidates exceed 15%, check your STUN reachability and UDP firewall rules. Also ensure you're preferring host/srflx over relay and that your SFU is directly reachable (no extra NAT in front of it). Moving TURN to UDP-first with TCP/443 as a fallback alone cut one client's relay bandwidth by 38%.

What's a realistic latency budget for WebRTC over a CDN edge network?

End-to-end for a well-tuned edge SFU cascade is 90-180ms. Budget roughly: 15-30ms last-mile, 20-40ms client to edge SFU, 30-60ms inter-PoP cascade (if needed), 20-35ms jitter buffer + decode. If you're seeing consistent p95 over 250ms, look at jitter buffer bloat, inter-SFU routing via public internet, or clients stuck on TURN-TCP. Keep packet loss under 1% and jitter under 25ms for clean Opus audio — video can tolerate a bit more, but audio is where users notice first.

Related Articles

References & Standards: HTTP Working Group Specifications · web.dev Articles · IETF Datatracker