Every streaming product hits a moment when the RTC bill stops being a rounding error. Here is how we moved a 50K-concurrent platform to our own SFUs — with zero downtime and better latency.

Why teams move off managed RTC vendors

When a streaming product crosses roughly ten thousand concurrent users, the RTC (real-time communication) bill stops being a rounding error. Managed vendors are wonderful when you are validating a market, but their pricing tiers and per-minute egress fees grow faster than your optimism. The classic breaking point: a launch night where your biggest cost line became whoever routed your users' audio.

The alternative is self-hosting WebRTC infrastructure. You run your own SFU (selective forwarding unit) mesh, your own signaling, your own token service, and your own egress for recordings and broadcasting. You keep the same thin SDK on the client — the app keeps calling the same join and publish APIs — but the media plane now lives on hardware you control.

This is not an all-or-nothing decision. Half the projects we ship use self-hosted SFUs for the core rooms and keep vendor minutes for global edge egress. The other half go fully self-hosted. Both are legitimate. The mistake is choosing either path without running the numbers first.

The real cost model

Managed pricing usually has four components: active room minutes, server-side egress, recordings, and geographic routing fees. For a 50K-concurrent platform in South Asia, the per-minute cost multiplies across every viewer who is not on the free tier. Self-hosting flips the model: you pay for compute and bandwidth. In our production numbers the shift reduced infrastructure cost between 40% and 60% at the same concurrency. Latency also improved, because the SFU sat closer to the users instead of routing to distant regional nodes.

When self-hosting is the wrong call

Be honest about the baseline. If your platform does a few hundred concurrent rooms and you have no team with WebRTC experience, the management overhead is rarely worth it. Self-hosting buys predictability and flexibility — it does not buy you time. You need someone who can debug a flaky ICE candidate or a clock-skewed SFU at 3 AM. If that person does not exist yet, budget for hiring or contracting one before you start.

Reference architecture: SFU + egress

The production topology we run for mid-size platforms is intentionally boring. A small set of SFU nodes — usually three to six, sized with a rough 100–200 concurrent streams per node — sit behind a load balancer. A signaling service accepts WebSocket connections, authenticates tokens, and tells each client which SFU and room to join. Media never touches the signaling tier; it flows directly between clients and the nearest SFU.

For recording or secondary output, an egress service subscribes to the room as an anonymous participant and forwards the stream onward. Keeping egress separate from the interactive room path is important: a recording job that spikes CPU should never degrade the live experience.

Hardware sizing, not exact science

SFU capacity depends much more on bandwidth than CPU. A single modern server with a 10 Gbps NIC comfortably handles several thousand concurrent subscribers in audio-only rooms and a few hundred in full-video rooms. We size for the "max worst case viewer" number and keep 30% headroom, then auto-scale stateless replicas behind a sticky-load-balanced entry point.

The token service: the part everyone forgets

The most common security mistake we inherit is "anyone with the room ID can join." Every join in our architecture requires a short-lived token signed by the backend. The token encodes the user identity, the room, the role, and an expiry — usually fifteen minutes to an hour. Rooms are created server-side; clients never mint their own credentials.

// Go — issue a room join token
func IssueRoomToken(userID, roomID, role string, ttl time.Duration) (string, error) {
    claims := jwt.MapClaims{
        "sub":    userID,
        "room":   roomID,
        "role":   role,
        "exp":    time.Now().Add(ttl).Unix(),
    }
    return jwt.NewWithClaims(jwt.SigningMethodHS256, claims).
        SignedString([]byte(roomSecret))
}

Rotation matters. We rotate the signing secret on every deploy and store per-tenant keys so one leaked secret cannot compromise another platform sharing our cluster.

Migration steps, in order

A good migration is a series of reversible steps, not one dramatic cutover:

  • Instrument both the current vendor and your own SFU with the same metrics: join success rate, p95 join latency, packet loss, bitrate.
  • Run the self-hosted stack in shadow mode — the same real users, but media mirrored to your SFU — for at least a week.
  • Allowlist a percentage of rooms to the new path. Start with 5% and watch error rates for a day.
  • Cut over the remaining rooms in waves during low-traffic windows, keeping the vendor fallback reachable for 30 days.
  • Turn off the old integration only after the fallback window passes with zero rollbacks.

Pitfalls we hit so you do not

  • NAT behaviour differs by carrier. Test with users on cellular and CGNAT-heavy ISPs, not only your office Wi-Fi.
  • Clock skew breaks token checks. Keep the SFU and signaling services on NTP; five seconds of skew will produce mysterious auth failures.
  • Egress saturates the same NIC as the room traffic. Dedicate egress to separate nodes or a dedicated interface.
  • Recording is a data-compliance decision. Decide jurisdiction and retention before you promise "recordings", not after a subpoena lands.

"Our users did not know we migrated — they just noticed lower latency."

— Sajeeb, Founder & Lead Architect, Gravity Compile

Zero-downtime cutover tactics

Remove the word "cutover" from your vocabulary and replace it with "gradual shift." A good shift starts with a canary room list, moves through tenant allowlists, and always keeps the previous route alive long enough to roll back cleanly. Our record was a platform with 50K concurrent users moved in four evening windows, each preceded by a full load rehearsal on staging hardware that mirrored production traffic patterns.

Rehearse failure, not just success

The rehearsals that caught the most problems were not the ones where everything worked. We deliberately killed an SFU node, rotated the token secret mid-session, and ran a room with double the planned viewer count. If those do not degrade gracefully in staging, they will not do it in production — and the difference between a graceful degrade and a full outage is usually a monitoring alert that fires while the problem is still a hypothesis. Add circuit breakers at the signaling layer, green/blue node definitions in your orchestrator, and a documented "abort and revert" sequence that every on-call engineer can run from a checklist.

Conclusion

Self-hosted WebRTC is a cost and control play, and it pays off when you have the operational discipline to run it well. Start with the token service, shadow-mode traffic, and honest metrics. If you would rather focus on product while the real-time layer is handled by people who do this daily, we run our own real-time platform — Gravix Cloud — and we also build custom WebRTC infrastructure for teams that want it under their own roof.