All articles

Production case study

How I Architected WebRTC Live Streaming and Delayed HLS Playback for 5,000+ Concurrent Platform Users

How I Architected WebRTC Live Streaming and Delayed HLS Playback for 5,000+ Concurrent Platform Users

At HonarAmoozesh, I worked as a Solutions Architect on an educational media platform serving more than 5,000 concurrent platform users. My responsibility was to design a hybrid media and signaling architecture around LiveKit, WebRTC, NATS JetStream, and Go, with HLS used only for playback available hours after the live session.

This case study describes the verified architecture boundaries and the reasoning behind them. It intentionally does not disclose proprietary deployment details or claim measurements I cannot substantiate publicly.

What the 5,000-user figure means

The verified figure is platform-level concurrency: more than 5,000 users could be present on the educational service at the same time. It does not mean 5,000 simultaneous publishers, 5,000 users in one room, or 5,000 WebRTC uplinks terminating on one SFU. Those are different load shapes and should never be treated as interchangeable.

This distinction drove the design. Teachers and active students needed immediate, bidirectional media. HLS playback was deliberately delayed by hours and could not substitute for a live class.

The central architecture decision

I designed a live WebRTC architecture with a separate delayed-playback path:

  • LiveKit and WebRTC for teachers, moderators, and participants who needed real-time interaction
  • HLS playback for material available hours after the session

HLS was not a fallback for current-session viewers and did not replace the live path. The key boundary was temporal: preserve WebRTC where immediate, bidirectional media mattered; provide HLS only where delayed playback met the product need.

That separation is the most important lesson from the project. Scaling a media platform is not only an SFU capacity problem. It is a classification problem: determine who must participate in the real-time session and who only needs to consume it.

Signaling as a separate system

Media routing and application signaling solve different problems. LiveKit handled the WebRTC media sessions. NATS JetStream provided the signaling backbone used by the surrounding application services, while Go was part of the service layer coordinating those workflows.

Keeping those responsibilities explicit made the design easier to reason about:

  • media state belonged to the real-time media layer
  • application events belonged to the signaling and service layer
  • delayed playback belonged to the HLS path
  • transitions between audience roles had to be deliberate

This avoids making the message bus pretend to be a media router or making the SFU carry application state it does not own.

A reusable promotion-boundary pattern

The public evidence for this project does not establish that live switching between a passive-viewer path and an interactive WebRTC role was deployed. The following is a reusable design pattern, not a claim about the HonarAmoozesh production implementation.

Where a passive viewer needs to become interactive, that transition is not just a UI toggle. The system would need to establish the real-time session, confirm that the participant is ready, and only then treat the user as active. The reverse transition would also need to release real-time resources rather than leaving an inactive participant attached indefinitely.

A reusable pattern is a small state machine:

  1. viewer requests an interactive role
  2. application policy authorizes the transition
  3. signaling coordinates session creation
  4. the client joins the LiveKit room
  5. application state changes only after media readiness
  6. demotion releases real-time resources; delayed playback, when available, remains a separate post-session experience

The exact policy and topology are product-specific. The ordering is not. Distributed systems become fragile when business state says a transition succeeded before the dependent media state exists.

Failure modes considered in the design

The architecture had to distinguish failures rather than collapse them into a generic connection error:

  • delayed HLS playback may be available hours later, but it does not substitute for a live WebRTC session
  • signaling can be available while a media session is not ready
  • a client can reconnect without being allowed to duplicate an active role
  • a role transition can time out after policy approval but before media readiness
  • passive viewers should not consume interactive capacity after a demotion

These are boundary failures. Clear ownership and idempotent transitions matter more than adding another retry loop.

What I can and cannot claim publicly

I can substantiate the platform-level concurrency figure, my Solutions Architect role, the live WebRTC architecture and delayed post-session HLS playback, and the use of LiveKit, NATS JetStream, and Go for coordination.

I am not presenting proprietary node counts, regional topology, exact latency, uptime, infrastructure cost, or load-test methodology as public facts. I am also not claiming that every concurrent user was an active WebRTC participant, that they were publishers, that they occupied one room, or that they terminated on one SFU. Nor am I claiming that live switching between WebRTC and HLS was implemented. This limitation is important because a useful engineering case study should make its evidence boundary visible.

Reusable checklist

For another educational or event platform, I would begin with these questions:

  • Which roles genuinely need bidirectional real-time media?
  • Is delayed HLS playback appropriate for post-session access without being presented as a live fallback?
  • Who owns room policy, media state, and application state?
  • Is promotion to an interactive role an explicit, observable state transition?
  • Can retries create duplicate participants or stale authorization?
  • Are capacity figures separated by publishers, subscribers, rooms, and total platform concurrency?
  • Does every published performance claim identify the load shape it measured?

The result is not a universal topology. It is a safer way to decide where real-time infrastructure creates value and where a broadcast path is the more reliable choice.

Closing perspective

The HonarAmoozesh project reinforced a principle I use in distributed-system design: scale by separating workloads with different consistency, latency, and interaction requirements before scaling every component uniformly.

For this platform, that meant preserving WebRTC for live interaction, using HLS only for delayed playback, and keeping application signaling explicit through NATS JetStream and Go. The 5,000-user result matters, but the more reusable engineering lesson is the boundary that made that concurrency shape manageable.

Related field notes