system-design49 min read

How to Design Live Streaming Platform like Twitch — A Senior+ Guide | Ayodhyya

How to Design Live Streaming Platform like Twitch — A Senior+ Guide

Building low-latency live video, chat, and creator monetization at 140M+ monthly user scale

📋 Tap to show Table of Contents

1. Introduction — Twitch at 140M+ Monthly Active Users

Live streaming has fundamentally transformed how humans consume entertainment, learn new skills, and build communities. At the center of this revolution sits Twitch — the world's premier live streaming platform owned by Amazon. As of 2026, Twitch commands over 140 million monthly active users, with over 9 million unique creators streaming every month and peak concurrent viewership regularly exceeding 10 million simultaneous connections.

The platform's influence extends far beyond gaming. While esports tournaments like League of Legends World Championship and The International draw millions of concurrent viewers, categories like Just Chatting, Creative, Music, and IRL streaming have broadened Twitch's appeal to mainstream audiences. At peak moments — such as major game launches or viral events — Twitch has recorded over 22 million concurrent viewers on a single day.

Building a Twitch-like platform is one of the most challenging system design problems in modern distributed systems. It requires solving multiple hard problems simultaneously:

  • Sub-second video latency from a broadcaster's camera to a viewer's screen across the globe
  • Millions of concurrent WebSocket connections for real-time chat with emotes, badges, and moderation
  • Adaptive bitrate transcoding of live video into 6+ quality tiers in real-time
  • Massive-scale CDN distribution of video segments to every continent
  • Real-time monetization through subscriptions, Bits (virtual currency), and ads
  • Content moderation at both live and VOD timescales
  • Discovery and recommendation algorithms to surface relevant live content

This article provides a comprehensive, senior-plus level system design walkthrough of building a live streaming platform comparable to Twitch. We will cover every major subsystem — from RTMP ingest to LL-HLS delivery, from IRC-based chat to Bits micro-transactions — with architecture diagrams, database schemas, API designs, and a complete 300+ line C# implementation.

Who is this guide for? This article targets senior and staff-level engineers preparing for system design interviews at top tech companies, as well as architects building real-time streaming infrastructure. We assume familiarity with distributed systems, video encoding fundamentals, and event-driven architectures.

2. Functional & Non-Functional Requirements

Functional Requirements

#RequirementDescription
F1Live BroadcastingStreamers can broadcast live video/audio using OBS, Streamlabs, or custom RTMP clients
F2Live ViewingViewers watch live streams with adaptive quality (240p to 1080p60/4K)
F3Live ChatReal-time messaging with emotes, badges, slow mode, subscriber-only mode
F4Channel Follow / SubscribeFollow channels, subscribe (Tier 1/2/3) for perks and emotes
F5Bits & CheeringVirtual currency (Bits) for cheering, animated emotes, and streamer revenue
F6Clip CreationViewers create and share 30–60 second clips from live or VOD content
F7VOD / ReplayPast broadcasts are automatically archived and searchable
F8Raid & HostStreamers can raid (send viewers to) other channels at stream end
F9Discovery & CategoriesBrowse by game/category, search streamers, recommended live channels
F10Content ModerationAuto-moderation of chat and VOD with report system and human review
F11Creator DashboardStream analytics, viewer count, chat activity, revenue breakdown

Non-Functional Requirements

PropertyTargetRationale
Availability99.99% (52 min downtime/year)Live events are time-sensitive; downtime during esports finals is unacceptable
Latency (ingest→playout)<3 seconds (LL-HLS), 5–8 sec (standard HLS)Interactive streaming requires near real-time for chat engagement
Chat Latency<500ms end-to-endChat must feel instant for interactive experience
Throughput10M+ concurrent viewers, 1M+ concurrent streamsPeak esports events can draw 20M+ viewers
Durability99.999999999% (11 nines) for VOD storageCreator content is irreplaceable; loss damages trust
ScalabilityAuto-scale to 10x normal load within 5 minutesViral streams or major events create sudden demand spikes
Global ReachSub-100ms TTFB from edge CDN nodes worldwideViewers in every continent expect fast stream start

3. Capacity Estimation & Back-of-Envelope

Key Assumptions

  • 140M monthly active users (MAU)
  • 10M daily active users (DAU) — ~7% of MAU
  • 1M concurrent peak viewers
  • 500K concurrent peak broadcasters
  • Average stream duration: 4 hours
  • Average viewer session: 45 minutes

Bandwidth Estimation

Each viewer watching a 1080p60 stream consumes approximately 6 Mbps of data. At 1M concurrent viewers:

1,000,000 viewers × 6 Mbps = 6 Tbps peak egress bandwidth required.

With transcoding producing 6 bitrate tiers, the CDN must serve a total of:

6 Tbps (average viewers) + 2 Tbps (headroom) ≈ 8 Tbps peak CDN capacity.

Storage Estimation

Data TypeDaily VolumeStorage
Live video segments (origin)500K streams × 4 hrs × 15 sec segments~480K segments/day
VOD archives (1080p)500K streams × 4 hrs × 1.5 GB/hr~3 PB/day
Chat messages2 billion messages/day~4 TB/day (compressed)
Clip metadata + thumbnails5M clips/day~500 GB/day
Chat logs (compressed, warm)2 billion messages~2 TB/day

QPS Estimation

OperationQPS
Video segment fetches (CDN edge)1M viewers / 2 sec segment interval = ~500K QPS
Chat message ingest~50K messages/sec average, 200K burst
Chat fan-out reads50K msg/s × avg 500 viewers per channel = ~25M reads/sec
Viewer count updates1M viewers / 30 sec heartbeat = ~33K QPS
API requests (browse, search)~100K QPS
Clip creation~5K QPS average

4. Data Model Design

The data model for a Twitch-like platform spans several key entities with distinct access patterns and consistency requirements.

Core Entities

Stream

  • stream_id (UUID, PK) — unique identifier for each live session
  • channel_id (UUID, FK) — the broadcaster's channel
  • stream_key (hashed string) — secret key used to start an RTMP session
  • title (string) — stream title set by the creator
  • category_id (UUID, FK) — game/category being streamed
  • language (string) — primary language
  • is_live (boolean) — whether the stream is currently active
  • viewer_count (integer) — real-time concurrent viewers
  • started_at (timestamp) — when the stream went live
  • ingest_server_id (UUID, FK) — which ingest server the stream is connected to
  • transcoding_status (enum) — pending, active, failed
  • viewer_count_updated_at (timestamp) — last viewer count update

Channel

  • channel_id (UUID, PK)
  • user_id (UUID, FK) — the owning user
  • display_name (string)
  • follower_count (bigint) — denormalized count
  • subscriber_count (bigint)
  • subscription_tiers_enabled (jsonb) — which tiers are available
  • partner_status (enum) — affiliate, partner, none
  • mature_content (boolean)
  • created_at (timestamp)

Viewer Session

  • session_id (UUID, PK)
  • user_id (UUID, FK) — null for anonymous viewers
  • stream_id (UUID, FK)
  • joined_at (timestamp)
  • last_heartbeat_at (timestamp)
  • quality_preference (string) — 1080p, 720p, auto
  • watch_duration_seconds (integer)

Clip

  • clip_id (UUID, PK)
  • stream_id (UUID, FK) — source stream
  • creator_id (UUID, FK) — who created the clip
  • channel_id (UUID, FK) — the channel clipped
  • start_offset_ms (integer) — offset from stream start
  • duration_ms (integer) — clip length (up to 60s)
  • video_url (string) — URL to processed clip video
  • thumbnail_url (string)
  • view_count (bigint)
  • title (string)
  • created_at (timestamp)

Subscription

  • subscription_id (UUID, PK)
  • user_id (UUID, FK) — subscriber
  • channel_id (UUID, FK) — subscribed channel
  • tier (enum) — tier_1, tier_2, tier_3
  • status (enum) — active, canceled, past_due, gifted
  • billing_cycle_start (timestamp)
  • gift_sender_id (UUID, nullable)
  • created_at (timestamp)

Follow

  • user_id (UUID, FK)
  • channel_id (UUID, FK)
  • created_at (timestamp)
  • Composite PK: (user_id, channel_id)

Relationship Diagram

erDiagram USER ||--o{ CHANNEL : owns CHANNEL ||--o{ STREAM : broadcasts USER ||--o{ VIEWER_SESSION : watches STREAM ||--o{ VIEWER_SESSION : has USER ||--o{ CLIP : creates CHANNEL ||--o{ CLIP : generates USER ||--o{ SUBSCRIPTION : purchases CHANNEL ||--o{ SUBSCRIPTION : receives USER ||--o{ FOLLOW : grants CHANNEL ||--o{ FOLLOW : receives STREAM ||--o{ CHAT_MESSAGE : contains CHANNEL ||--o{ CHAT_MESSAGE : moderates USER { uuid user_id PK string username string email string password_hash timestamp created_at } CHANNEL { uuid channel_id PK uuid user_id FK string display_name bigint follower_count enum partner_status } STREAM { uuid stream_id PK uuid channel_id FK string title boolean is_live int viewer_count timestamp started_at } CLIP { uuid clip_id PK uuid stream_id FK uuid creator_id FK int duration_ms bigint view_count } SUBSCRIPTION { uuid subscription_id PK uuid user_id FK uuid channel_id FK enum tier enum status }

5. API Design

REST API Endpoints

MethodEndpointDescriptionAuth
POST/api/v1/streams/ingestBroadcaster initiates RTMP session, receives stream keyRequired
PATCH/api/v1/streams/:idUpdate stream title, category, languageRequired
DELETE/api/v1/streams/:idEnd live broadcastRequired
GET/api/v1/streams/:idGet stream metadata and viewer countPublic
GET/api/v1/streams?category=&language=&first=Browse live streams with filters and paginationPublic
POST/api/v1/channels/:id/followFollow a channelRequired
POST/api/v1/channels/:id/subscribeSubscribe to a channelRequired
GET/api/v1/clips?stream_id=&first=List clips for a streamPublic
POST/api/v1/clipsCreate a new clipRequired
GET/api/v1/streams/:id/video.m3u8LL-HLS manifest for playbackPublic

WebSocket Protocol for Chat

Chat Connection Flow

  1. Client connects to wss://chat.example.com/ws?channel=channel_name&auth=token
  2. Server authenticates token and joins the user to the channel's chat room
  3. Server sends ROOMSTATE message with room configuration
  4. Client sends PRIVMSG to post messages
  5. Server broadcasts messages to all connected viewers via pub/sub fanout
  6. Client sends periodic PING to maintain connection

Chat Message Types

TypeDirectionPayload
PRIVMSGClient → Server{ channel, message, tags }
PRIVMSGServer → Clients{ channel, user, message, badges, emotes, color }
PING / PONGBidirectional{}
ROOMSTATEServer → Client{ slow_mode, sub_only, emote_only, follower_only }
CLEARCHATServer → Clients{ channel, target_user, duration }
USERNOTICEServer → Clients{ channel, type, message, sub_tier }
NOTICEServer → Client{ channel, message_id, text }

6. High-Level Architecture

graph TB subgraph "Creator Side" OBS["OBS / Streamlabs
RTMP Encoder"] CDN["Upload CDN
(Ingest PoPs)"] end subgraph "Ingest Layer" IS1["Ingest Server 1
(RTMP接收)"] IS2["Ingest Server 2
(RTMP接收)"] IS3["Ingest Server N
(RTMP接收)"] SKM["Stream Key
Manager"] end subgraph "Processing Pipeline" TP["Transcoding
Pipeline"] TR1["Transcoder
GPU Instance"] TR2["Transcoder
GPU Instance"] TR3["Transcoder
GPU Instance"] QA["Quality
Analyzer"] end subgraph "Storage" S3["Object Storage
(S3 / GCS)"] REDIS["Redis Cluster
(Chat, Sessions)"] PG["PostgreSQL
(Users, Channels)"] CASS["Cassandra
(Chat Logs)"] ES["Elasticsearch
(Search, Discovery)"] end subgraph "CDN & Delivery" EDG1["CDN Edge
North America"] EDG2["CDN Edge
Europe"] EDG3["CDN Edge
Asia Pacific"] end subgraph "Services" US["User Service"] SS["Stream Service"] CS["Chat Service"] BS["Billing Service"] MS["Moderation Service"] DS["Discovery Service"] VS["Viewer Count
Service"] CR["Clip Service"] end subgraph "Viewer Side" VW1["Web Browser
(HLS.js)"] VW2["Mobile App
(AVPlayer)"] VW3["Smart TV
(ExoPlayer)"] end OBS --> CDN --> IS1 & IS2 & IS3 IS1 & IS2 & IS3 --> SKM IS1 & IS2 & IS3 --> TP TP --> TR1 & TR2 & TR3 TR1 & TR2 & TR3 --> QA QA --> S3 S3 --> EDG1 & EDG2 & EDG3 EDG1 & EDG2 & EDG3 --> VW1 & VW2 & VW3 US --> PG SS --> REDIS CS --> REDIS CS --> CASS DS --> ES VS --> REDIS CR --> S3

Architecture Summary

The architecture follows a pipeline pattern: video flows from the broadcaster through ingest servers, through a transcoding pipeline, into object storage, and out through a global CDN to viewers. Chat operates as a separate real-time system using WebSocket connections fanned out through a pub/sub message bus. Metadata services (users, channels, discovery, billing) form the API layer backed by relational and search databases.

7. Ingest Server & Stream Key System

The ingest layer is the broadcaster's entry point into the platform. It must accept RTMP connections from encoding software (OBS, Streamlabs, XSplit) and authenticate them using a stream key — a secret token that proves the broadcaster owns the channel.

Stream Key Architecture

sequenceDiagram participant Broadcaster as Broadcaster (OBS) participant API as Stream API participant SKM as Stream Key Manager participant Redis as Redis Cache participant Ingest as Ingest Server Broadcaster->>API: POST /api/v1/streams/ingest API->>API: Authenticate user token API->>SKM: Generate stream key SKM->>SKM: Generate UUID + HMAC signature SKM->>Redis: Cache key → channel_id mapping (TTL=24h) SKM-->>API: Return stream_key, ingest_url API-->>Broadcaster: { ingest_url: "rtmp://ingest-us1.example.com/live", stream_key: "live_abc123..." } Broadcaster->>Ingest: RTMP Publish (stream_key) Ingest->>Redis: Validate stream_key Redis-->>Ingest: channel_id, permissions Ingest->>Ingest: Start receiving frames Ingest->>API: POST /api/v1/streams (is_live=true) API-->>Ingest: stream_id

Stream Key Design

Key Format: live_{random_32_bytes_hex}_{hmac_signature}
Rotation: Keys can be regenerated at any time; old keys are invalidated with a 60-second grace period for in-flight streams.
Security: Keys are stored as salted hashes; only the plaintext is shown once upon creation. Rate limiting prevents brute-force guessing.

Ingest Server Responsibilities

  1. RTMP Handshake & Authentication: Validate the stream key against the cached mapping, rejecting unauthorized attempts within <100ms.
  2. Frame Reception: Accept H.264/AVC or H.265/HEVC encoded video frames and AAC audio at the broadcaster's configured bitrate.
  3. Health Monitoring: Track bitrate stability, dropped frames, and connection quality. Alert if bitrate drops below threshold.
  4. Origin Flash: Begin forwarding video segments to the transcoding pipeline within 2 seconds of receiving the first keyframe.
  5. Redundancy: If a broadcaster sends to multiple ingest servers simultaneously, the system selects the best source and ignores duplicates.

Ingest Server Load Balancing

We use latency-based DNS routing (e.g., Route 53 latency-based routing or Cloudflare Load Balancer) to direct broadcasters to the nearest ingest PoP. Within each PoP, we use consistent hashing on the stream key to ensure all frames from a single stream land on the same ingest server.

8. RTMP & Low-Latency Streaming (LL-HLS, LL-DASH)

RTMP Ingest Protocol

RTMP (Real-Time Messaging Protocol) remains the dominant protocol for live stream ingest. Despite its age (originally developed by Macromedia/Adobe in 2012), RTMP offers:

  • Low-latency TCP-based delivery with typically <1 second of latency on the ingest side
  • Widespread support in all major encoders (OBS, Streamlabs, FFmpeg)
  • Reliable delivery with automatic reconnection
  • Support for video codecs: H.264, H.265/HEVC (enhanced RTMP)
  • Audio codec support: AAC, Opus

Modern RTMP implementations also support Enhanced RTMP and SRT (Secure Reliable Transport) as an alternative ingest protocol offering better performance over unreliable networks.

Playback Protocol Comparison

ProtocolLatencySegment DurationBrowser SupportAdaptability
Standard HLS15–30 sec6–10 secAll browsersExcellent
LL-HLS (Low-Latency HLS)2–4 sec0.5–2 secSafari native, others via HLS.jsExcellent
Standard DASH10–20 sec4–6 secAll browsersExcellent
LL-DASH (Low-Latency DASH)2–4 sec0.5–2 secAll browsers via dash.jsExcellent
WebRTC<1 secN/A (frame-level)All browsersLimited scalability

LL-HLS Deep Dive

Apple's Low-Latency HLS (LL-HLS) is the industry standard for low-latency live streaming. Key mechanisms:

  • Partial Segments (Parts): Each HLS segment is split into small parts (~200ms each), allowing the player to request partial segments before the full segment is complete.
  • Preload Hints: The manifest includes a #EXT-X-PRELOAD-HINT tag pointing to the next partial segment that hasn't been generated yet, enabling proactive fetching.
  • Blocking Playlist Reload: The player can ask the server to hold the HTTP connection until the next update, eliminating polling overhead.
  • Rendition Reports: Compact manifest updates for other renditions to minimize playlist download overhead.
graph LR subgraph "Transcoder Output" SEG["Video Segments
(1-2 sec)"] PARTS["Partial Segments
(~200ms each)"] M3U8["Master Playlist
(.m3u8)"] end subgraph "LL-HLS Manifest" M1["Master Playlist
#EXT-X-STREAM-INF"] M2["Media Playlist
#EXT-X-PART
#EXT-X-PRELOAD-HINT"] end subgraph "Player Requests" P1["1. Fetch Master"] P2["2. Fetch Media Playlist"] P3["3. Fetch Parts"] P4["4. Preload Hint"] P5["5. Blocking Reload"] end SEG --> M3U8 PARTS --> M2 M3U8 --> M1 M1 --> P1 --> P2 --> P3 --> P4 --> P5

9. Transcoding Pipeline (Adaptive Bitrate)

Transcoding is the most compute-intensive part of the live streaming pipeline. Each incoming stream must be transcoded into multiple renditions (quality tiers) in real-time to support adaptive bitrate streaming.

Transcoding Output Renditions

ResolutionFPSVideo BitrateAudio BitrateCodec
1920×1080606,000 kbps160 kbpsH.264 High
1920×1080304,500 kbps128 kbpsH.264 High
1280×720603,000 kbps128 kbpsH.264 Main
1280×720302,000 kbps96 kbpsH.264 Main
854×480301,000 kbps64 kbpsH.264 Main
640×36030600 kbps64 kbpsH.264 Baseline
426×24030300 kbps48 kbpsH.264 Baseline

Transcoding Pipeline Architecture

graph TB subgraph "Ingest Layer" RTMP_IN["RTMP Stream
(Broadcast Quality)"] end subgraph "Segmenter" SEG["Stream Segmenter
(GStreamer / FFmpeg)"] end subgraph "Transcoding Farm" direction TB MASTER["Transcoding
Orchestrator"] GPU1["GPU Worker 1
(NVIDIA T4)"] GPU2["GPU Worker 2
(NVIDIA T4)"] GPU3["GPU Worker 3
(NVIDIA T4)"] CPU1["CPU Worker 1
(x264 superfast)"] end subgraph "Packager" HLS_P["HLS Packager
(LL-HLS + Standard)"] THUMB["Thumbnail
Generator"] end subgraph "Output" S3["Object Storage
(S3)"] CDN["CDN Origin"] PREVIEW["Preview
Thumbnails"] end RTMP_IN --> SEG SEG --> MASTER MASTER --> GPU1 & GPU2 & GPU3 & CPU1 GPU1 & GPU2 & GPU3 & CPU1 --> HLS_P SEG --> THUMB HLS_P --> S3 --> CDN THUMB --> PREVIEW HLS_P --> PREVIEW
GPU vs CPU Transcoding: A single NVIDIA T4 GPU can transcode one 1080p60 stream into all 7 renditions simultaneously using NVENC. For 500K concurrent streams, we need approximately 500K GPU instances — but in practice, most streams use lower quality settings and we can batch encode, reducing the requirement to ~100K GPU-equivalents with preemption support.

Transcoding Scheduling

The Transcoding Orchestrator manages a work queue of active streams requiring transcoding. When a new RTMP session starts:

  1. The orchestrator assigns the stream to a GPU worker based on resource availability and geographic proximity to the ingest server.
  2. The GPU worker initializes the FFmpeg/NVENC pipeline with the appropriate encoding parameters.
  3. Segmented output (HLS parts and segments) is written to object storage with a prefix of /{channel_id}/{stream_id}/.
  4. The master playlist (index.m3u8) is updated in real-time as new segments are produced.
  5. When the broadcaster disconnects, the orchestrator signals the worker to flush remaining frames and finalize the VOD.

10. Content Delivery Network for Live

Delivering live video to millions of concurrent viewers requires a deeply distributed CDN optimized for the unique access patterns of live content.

CDN Architecture for Live Streaming

graph TB subgraph "Origin Layer" ORIGIN["Origin Server
(Object Storage)"] end subgraph "Mid-Tier Cache" MID_US["Mid-Tier
US-East"] MID_EU["Mid-Tier
EU-West"] MID_AP["Mid-Tier
Asia-Pacific"] end subgraph "Edge Layer" E1["Edge PoP
New York"] E2["Edge PoP
London"] E3["Edge PoP
Tokyo"] E4["Edge PoP
Mumbai"] E5["Edge PoP
São Paulo"] E6["Edge PoP
Sydney"] end subgraph "Viewers" V1["500K Viewers
US"] V2["300K Viewers
EU"] V3["200K Viewers
APAC"] end ORIGIN --> MID_US & MID_EU & MID_AP MID_US --> E1 & E5 MID_EU --> E2 MID_AP --> E3 & E4 & E6 E1 & E5 --> V1 E2 --> V2 E3 & E4 & E6 --> V3

CDN Caching Strategy for Live

Live streaming CDN caching differs significantly from traditional CDN caching:

  • Short TTL: Live HLS segments have a TTL of only 2–5 seconds at edge, ensuring freshness.
  • Cache-Control: Playlist files use Cache-Control: max-age=1; segments use max-age=86400 (they're immutable once created).
  • Range Requests: For LL-HLS partial segments, the CDN must support HTTP range requests to serve byte-range portions of segment files.
  • Connection Coalescing: CDN edges multiplex requests for the same segment from multiple viewers into a single origin fetch (request collapsing).
  • Grace Mode: If the origin is temporarily unreachable, the CDN serves stale segments with a stale-while-revalidate directive rather than returning errors.
Cache Stampede Mitigation: When a popular streamer goes live, thousands of viewers simultaneously request the first playlist. We use probabilistic early recomputation and request coalescing at CDN edges to prevent origin overload.

11. Live Chat System (IRC-Based Pub/Sub)

Twitch's chat system is one of the most iconic features of the platform. It must handle 50,000+ messages per second during popular streams with sub-500ms delivery latency to all connected viewers.

Chat System Architecture

graph TB subgraph "Client Layer" C1["Web Client
(WebSocket)"] C2["Mobile Client
(WebSocket)"] C3["IRC Client
(IRC Protocol)"] end subgraph "Chat Gateway" GW1["Gateway Node 1"] GW2["Gateway Node 2"] GW3["Gateway Node N"] AUTH["Auth Service"] end subgraph "Chat Service Layer" MR["Message Router"] PS["Pub/Sub Bus
(NATS / Kafka)"] MOD["Moderation
Filter"] EM["Emote
Service"] end subgraph "Storage" REDIS_CHAT["Redis Pub/Sub
(Hot Rooms)"] CASSANDRA["Cassandra
(Chat Log Archive)"] CRDB["CockroachDB
(Room Config)"] end C1 & C2 & C3 --> GW1 & GW2 & GW3 GW1 & GW2 & GW3 --> AUTH GW1 & GW2 & GW3 --> MR MR --> PS MR --> MOD MOD --> EM PS --> REDIS_CHAT REDIS_CHAT --> CASSANDRA CRDB --> MR

Chat Message Processing Pipeline

sequenceDiagram participant Viewer as Viewer participant GW as Chat Gateway participant Auth as Auth Service participant MR as Message Router participant MOD as Moderation participant PS as Pub/Sub participant Fans as Fanout Workers Viewer->>GW: Send PRIVMSG GW->>GW: Validate connection GW->>Auth: Check permissions Auth-->>GW: OK (badges, permissions) GW->>MR: Route to room MR->>MOD: Check content alt Message passes moderation MOD->>PS: Publish to room channel PS->>Fans: Fan out to subscribers Fans->>Fans: Batch messages (50ms window) Fans-->>Viewer: Broadcast PRIVMSG to all else Message blocked MOD-->>GW: BLOCKED + reason GW-->>Viewer: NOTICE (blocked) end

Chat Room Fan-Out Strategy

For a channel with 500K concurrent chatters, sending each message individually to all viewers is prohibitively expensive. We use a multi-tier fan-out approach:

  1. Room Sharding: Each active chat room is assigned to a specific Pub/Sub topic. Messages are published once to the topic.
  2. Gateway Grouping: Chat gateways are grouped into pools of ~50 nodes. Each pool subscribes to a subset of active rooms.
  3. Message Batching: Messages are batched into 50ms windows and sent as a single WebSocket frame, reducing per-message overhead by 10–50x.
  4. Backpressure: Viewers who cannot keep up (slow consumers) receive message drops rather than buffer bloat.
Scaling Insight: For a room with 500K viewers across 50 gateway nodes, each node handles ~10K connections. Each inbound message is published once, then the gateway pool delivers it to all 50K connections in their pool — resulting in 50K writes per gateway node per message, not 500K. This reduces the per-node fan-out by 10x.

Chat Rate Limiting

Limit TypeRateScope
Global message rate20 messages/30 sec per userAll rooms
Room slow modeConfigurable (5–120 sec between messages)Per room
Subscription-only modeOnly subscribers can send messagesPer room
Follower-only modeMust follow for N minutes before chattingPer room
Emote-only modeOnly emotes allowed in messagesPer room
API rate limit800 messages/30 sec per user (IRC)API-wide

12. Viewership & Concurrent Viewer Counting

Accurate real-time viewer counts are critical for the platform — they drive discovery rankings, creator analytics, and ad revenue calculations. However, counting concurrent viewers at scale is deceptively complex.

Viewer Counting Architecture

graph LR subgraph "Viewer Clients" V1["Viewer 1"] V2["Viewer 2"] V3["Viewer N"] end subgraph "Heartbeat Layer" HB["Heartbeat
Collector Service"] R["Redis Sorted Sets
stream_id → {viewer_id: timestamp}"] end subgraph "Counting Service" CS["Counter Service
(Approximate Counting)"] RC["Rolling Counter
(30-sec window)"] end subgraph "Downstream" API["Stream API
(viewer_count field)"] DISC["Discovery Service
(ranking)"] ANALYTICS["Analytics
(viewership charts)"] end V1 & V2 & V3 -->|heartbeat every 30s| HB HB --> R HB --> CS CS --> RC RC --> API RC --> DISC RC --> ANALYTICS

Counting Algorithm

We use a sliding window counter with heartbeats:

  1. Each viewer client sends a heartbeat every 30 seconds containing their session ID and the stream ID they're watching.
  2. The Heartbeat Collector inserts/updates the viewer's timestamp in a Redis Sorted Set keyed by viewers:{stream_id} with the current timestamp as the score.
  3. A background process periodically removes entries older than 90 seconds (3 missed heartbeats) from the sorted set.
  4. The concurrent viewer count is the cardinality of the sorted set, computed using ZCARD or HyperLogLog for approximate counting at very high scales.

Accuracy vs Performance Trade-off

  • Precise counting: Use Redis Sorted Sets with ZCARD — O(1) per stream, accurate to ±0.1%. Works for up to ~10M concurrent viewers across all streams.
  • Approximate counting: Use HyperLogLog for aggregate counts, or a sampling-based approach where we count a statistical sample of viewers and extrapolate.
  • Consistency: Viewer counts are eventually consistent with a 30-second propagation delay. This is acceptable because exact real-time counts are neither expected nor necessary.

13. Clip Creation & VOD System

Clip Creation Pipeline

sequenceDiagram participant Viewer as Viewer participant API as Clip API participant Q as Clip Processing Queue participant W as Clip Worker participant S3 as Object Storage participant CDN as CDN Viewer->>API: POST /api/v1/clips {stream_id, offset_ms, title} API->>API: Validate viewer has permission API->>API: Generate clip_id, record metadata API->>Q: Enqueue clip job API-->>Viewer: {clip_id, status: "processing"} Q->>W: Dequeue clip job W->>W: Fetch segment from origin/VOD W->>W: Transcode clip to multiple qualities W->>W: Generate thumbnail (frame at 2s) W->>S3: Upload processed clip + thumbnail S3->>CDN: Invalidation → propagation W->>API: Update clip status → "ready" API->>API: Set clip_url to CDN path

VOD (Past Broadcast) System

When a streamer ends their broadcast, the system must:

  1. Finalize Segments: The transcoding pipeline flushes any in-progress segments and writes the final VOD playlist.
  2. Create VOD Record: A VOD entry is created with metadata (title, duration, thumbnail, category, tags).
  3. Migrate to Warm/Cold Storage: Recently active VODs remain on SSD-backed storage for fast access. Older VODs are transitioned to cheaper storage tiers (S3 Standard → S3 Infrequent Access → S3 Glacier).
  4. Re-encode for VOD: Background re-encoding creates optimized VOD renditions with longer segment durations (6 seconds instead of 2) for smoother playback.
  5. Index for Search: The VOD metadata is indexed in Elasticsearch for category browsing and search.

14. Creator Dashboard & Analytics

The Creator Dashboard provides streamers with real-time insights into their channel performance, viewer engagement, and revenue. It's a critical retention feature — creators who understand their audience grow faster.

Dashboard Components

ComponentData SourceUpdate Frequency
Live Viewer CountViewer Count Service (Redis)Real-time (30 sec)
Chat Activity FeedChat Service (WebSocket)Real-time
Chat Rate GraphChat Analytics (Time Series DB)Every 5 minutes
Follower GrowthFollower Service (Event Stream)Every 15 minutes
Revenue BreakdownBilling Service (PostgreSQL)Daily
Subscription BreakdownSubscription Service (PostgreSQL)Daily
Bits RevenueBilling Service (PostgreSQL)Daily
Average Watch TimeViewer Session ServiceEvery 5 minutes
Unique Viewers (24h)Viewer Session ServiceHourly
Top ClipsClip Service (Elasticsearch)Every 30 minutes

Real-Time Analytics Architecture

The creator dashboard uses a Lambda Architecture combining:

  • Speed Layer: Real-time metrics streamed through Apache Kafka → Apache Flink for streaming aggregations → WebSocket push to the dashboard.
  • Batch Layer: Daily aggregates computed via Apache Spark on historical data stored in the data lake (S3 + Parquet).
  • Serving Layer: Pre-computed aggregates served from a columnar database (ClickHouse or Amazon Redshift) for the dashboard UI.

15. Subscription & Bits Monetization

Subscription System

TierMonthly PriceRevenue to CreatorPlatform CutFeatures
Tier 1$4.99~$2.50 (50%)~$2.49Custom emotes, badge, ad-free
Tier 2$9.99~$5.00 (50%)~$4.99Everything in T1 + more emotes
Tier 3$24.99~$12.50 (50%)~$12.49Everything in T2 + premium emotes
Gift Sub (T1)$4.99~$2.50~$2.49Same as T1 for recipient

Bits (Virtual Currency) System

Bits are Twitch's virtual currency used for Cheering — a way for viewers to tip streamers with animated emotes in chat. The economics:

  • Purchase Rate: 1 Bit ≈ $0.01 for the viewer (bulk discounts apply)
  • Revenue Share: Streamers receive $0.01 per Bit received
  • Platform Revenue: Twitch earns the spread between the viewer purchase price and the creator payout (typically 20–40% margin depending on purchase volume)
  • Bits Threshold: Minimum 100 Bits to Cheer (100 Bits = $1.00)

Bits Transaction Flow

  1. Viewer purchases Bits via the Bits Shop (Stripe/PayPal integration)
  2. Bits balance is credited to the viewer's account (stored in Redis for fast reads)
  3. Viewer sends Cheer100 in chat (100 Bits)
  4. Chat service validates balance, deducts Bits, and broadcasts the Cheer with animated emotes
  5. A revenue event is created and the streamer's earnings balance is updated
  6. At payout time (monthly), the streamer's total Bits revenue is transferred to their linked bank account

16. Raid & Host System

Raiding is a social feature where a streamer sends their viewers to another channel at the end of their stream. It's a powerful discovery mechanism and community-building tool.

Raid System Design

sequenceDiagram participant StreamerA as Streamer A (Ending) participant API as Raid API participant ChatA as Chat A participant ChatB as Chat B participant ViewersA as Streamer A's Viewers participant ViewersB as Streamer B's Viewers StreamerA->>API: POST /api/v1/raids {target_channel: "StreamerB"} API->>API: Validate: A is live, B is live, A not self-raiding, cooldown check API->>API: Create raid_record API->>ChatA: Send USERNOTICE: "Raiding StreamerB with 15,000 viewers!" ChatA->>ViewersA: Broadcast raid notification par Parallel Operations API->>API: Update A's stream: raid_outgoing API->>API: Update B's stream: raid_incoming end API->>ChatB: Send USERNOTICE: "Raider from StreamerA has arrived!" ChatB->>ViewersB: Broadcast raid arrival Note over ViewersA: Auto-redirect viewers (optional) ViewersA->>ViewersA: Click "Join Raid" button ViewersA->>ViewersA: Navigate to Streamer B's stream

Raid Constraints

  • Cooldown: A streamer can only initiate a raid once per stream (prevents abuse)
  • Minimum viewers: At least 1 viewer required to raid
  • Target must be live: Can only raid active, non-banned channels
  • Rate limiting: Max 5 outgoing raids per 24 hours
  • Anti-abuse: Raid spam detection to prevent coordinated harassment raids

17. Discovery & Category Search

Discovery is how viewers find new content to watch. Twitch's discovery system combines algorithmic ranking, category browsing, and personalization to surface relevant live streams.

Discovery Ranking Factors

FactorWeightData Source
Viewer Count (Popularity)35%Viewer Count Service
Relevance to Category20%Category match + tags
User's Follow History15%User preference profile
Stream Freshness10%Time since stream started
Engagement Rate10%Chat activity / viewer ratio
Language Match5%User language settings
Partner/Affiliate Status5%Channel partner tier

Search Architecture

Search and category browsing are powered by Elasticsearch with the following indices:

  • streams: Active live streams with title, category, tags, viewer_count, language, partner_status
  • channels: All registered channels with follower_count, partner_status, display_name
  • categories: Game/category names with viewer counts and stream counts
  • clips: Archived clips with title, view_count, creator, channel

The search index is updated in real-time via CDC (Change Data Capture) from PostgreSQL, with a 1–2 second propagation delay.

18. Content Moderation (Live + VOD)

Content moderation at scale is essential for platform safety and legal compliance. Twitch employs a multi-layered moderation system covering both live streams and VOD content.

Moderation Layers

LayerTechnologyScopeLatency
AutoMod (Chat)ML Text ClassificationChat messages<100ms
Visual ModerationComputer Vision (CNN)Live stream frames2–5 sec
Audio ModerationSpeech-to-Text + NLPLive stream audio5–10 sec
VOD ReviewBatch ML processingArchived broadcastsMinutes–Hours
Human Review QueueModeration dashboardReported contentHours
Channel ModsVolunteer human modsIndividual channelsReal-time

AutoMod Chat Filter Pipeline

graph LR MSG["Chat Message"] --> TOKENIZE["Tokenizer"] TOKENIZE --> EMBED["Word Embeddings
(FastText)"] EMBED --> CLASS["ML Classifier
(LSTM/Transformer)"] CLASS --> SCORE["Confidence Score"] SCORE -->|"< 0.3"| PASS["✅ Allow"] SCORE -->|"0.3 - 0.7"| QUEUE["⚠️ Review Queue"] SCORE -->|"> 0.7"| BLOCK["🚫 Block + Notify"] QUEUE --> HUMAN["Human Moderator"] HUMAN -->|Approved| PASS HUMAN -->|Rejected| BLOCK
Context-Aware Moderation: The system considers context like the streamer's own content rating, channel maturity level, and community guidelines when making moderation decisions. A word flagged as offensive in one context may be acceptable in another.

19. Database Design & Sharding

Database Selection

DatabaseUse CaseWhy
PostgreSQLUsers, Channels, Subscriptions, BillingACID compliance, complex queries, relational integrity
RedisViewer sessions, chat rooms, rate limiting, cachingSub-millisecond reads, pub/sub, TTL support
CassandraChat message logs, viewer history, event logsWrite-heavy workload, time-series data, horizontal scaling
ElasticsearchSearch, discovery, clip indexingFull-text search, faceted filtering, relevance ranking
ClickHouseAnalytics, viewership dashboardsColumnar storage, fast aggregations on large datasets
Apache KafkaEvent streaming, CDCDurable event log, exactly-once semantics, fan-out

PostgreSQL Sharding Strategy

We shard the PostgreSQL cluster using hash-based sharding on user_id for user data, and channel_id for channel-related data. The sharding key is chosen based on the primary access pattern:

Sharding Decisions

  • Users table: Sharded by hash(user_id) % 1024 across 16 shards (64 partitions each). Most queries are user-scoped.
  • Channels table: Co-located with users table on the same shard to avoid cross-shard joins for channel→user lookups.
  • Subscriptions table: Sharded by hash(channel_id) since subscription queries are primarily channel-scoped.
  • Follows table: Sharded by hash(channel_id) with a secondary index on user_id using a materialized view.

Cassandra Schema for Chat Logs

Chat Message Table (Cassandra)

CREATE TABLE chat_messages (

  channel_id UUID,

  message_time TIMESTAMP,

  message_id UUID,

  user_id UUID,

  username TEXT,

  message TEXT,

  badges LIST<TEXT>,

  emotes LIST<TEXT>,

  PRIMARY KEY ((channel_id), message_time, message_id)

) WITH CLUSTERING ORDER BY (message_time DESC);

Partition key: channel_id | Clustering: message_time DESC

Query pattern: Get latest messages for a channel — efficient single-partition scan.

20. Caching Strategy

Multi-Level Cache Hierarchy

graph TB subgraph "L1: CDN Edge Cache" CDN1["CDN Edge
Video Segments
TTL: 2-5 sec (live)"] CDN2["CDN Edge
Static Assets
TTL: 24h"] end subgraph "L2: Application Cache" REDIS1["Redis Cluster
Viewer Sessions
TTL: 90 sec"] REDIS2["Redis Cluster
Stream Metadata
TTL: 30 sec"] REDIS3["Redis Cluster
User Profile
TTL: 5 min"] end subgraph "L3: Database Query Cache" PG_CACHE["PgBouncer
Connection Pooling"] ES_CACHE["Elasticsearch
Query Cache"] end subgraph "Origin" DB["PostgreSQL
Cassandra"] end CDN1 --> CDN2 CDN2 --> REDIS1 & REDIS2 & REDIS3 REDIS1 & REDIS2 & REDIS3 --> PG_CACHE & ES_CACHE PG_CACHE & ES_CACHE --> DB

Cache Key Design

Cache Key PatternValueTTLEviction
stream:{stream_id}:metaStream metadata JSON30 secLRU
stream:{stream_id}:viewersConcurrent viewer count60 secLRU
user:{user_id}:profileUser profile JSON5 minLRU
channel:{channel_id}:subsSubscriber count1 minLRU
chat:{channel_id}:configRoom settings2 minWrite-through
bits:{user_id}:balanceBits balance (integer)No TTL (infinite)Write-through
ratelimit:{user_id}:chatMessage count (sorted set)30 sec windowTTL-based

21. Multi-Region Design

Multi-Region Architecture

graph TB subgraph "US-EAST (Primary)" US_MASTER["PostgreSQL Primary
(Read/Write)"] US_REDIS["Redis Cluster
(Sessions)"] US_KAFKA["Kafka Cluster
(Events)"] US_INGEST["Ingest PoP
(NA Creators)"] end subgraph "EU-WEST (Secondary)" EU_REPLICA["PostgreSQL Replica
(Read-Only)"] EU_REDIS["Redis Cluster
(Replicated)"] EU_KAFKA["Kafka Mirror
(Replicated)"] EU_INGEST["Ingest PoP
(EU Creators)"] end subgraph "AP-SOUTHEAST (Secondary)" AP_REPLICA["PostgreSQL Replica
(Read-Only)"] AP_REDIS["Redis Cluster
(Replicated)"] AP_KAFKA["Kafka Mirror
(Replicated)"] AP_INGEST["Ingest PoP
(APAC Creators)"] end US_MASTER -->|Async Replication| EU_REPLICA & AP_REPLICA US_KAFKA -->|MirrorMaker| EU_KAFKA & AP_KAFKA US_REDIS -->|CRDT Replication| EU_REDIS & AP_REDIS

Region Routing Strategy

  • Broadcaster Routing: Ingest servers are selected based on broadcaster's geo-location via latency-based DNS. Each region operates independently for ingest.
  • Viewer Routing: Viewers are routed to the nearest CDN edge. CDN handles origin selection transparently.
  • Write Path: All write operations (stream creation, subscriptions, Bits) are routed to the US-EAST primary region. Writes to the primary are replicated asynchronously to secondary regions (RPO ≈ 1–2 seconds).
  • Read Path: Reads are served from the nearest regional replica for low latency. Non-critical reads (viewer count, follower count) use eventually consistent replicas.
  • Failover: If a secondary region loses connectivity, it can continue serving reads from cached data. If the primary fails, a secondary can be promoted to primary with a manual or automated failover process (RTO ≈ 30 seconds).

22. Cost Estimation

Monthly Infrastructure Cost Breakdown

ComponentInstance TypeQuantityMonthly Cost (USD)
Ingest Serversc5.2xlarge (8 vCPU, 16 GB)500$72,000
Transcoding GPUsg4dn.xlarge (T4 GPU)100,000$4,320,000
Chat Gateway Nodesm5.xlarge (4 vCPU, 16 GB)200$28,800
API Serversc5.xlarge (4 vCPU, 8 GB)100$14,400
Redis Clusterr6g.2xlarge (8 vCPU, 52 GB)50$16,800
PostgreSQL (RDS)db.r6g.4xlarge (16 vCPU, 128 GB)32 shards$43,008
Cassandra Clusteri3.4xlarge (16 vCPU, 122 GB)200$120,000
Elasticsearchr5.xlarge.elasticsearch60$18,000
Kafka Clusterkafka.m5.2xlarge30$18,000
CDN Egress (8 Tbps peak)CloudFront / Fastly~20 PB/month$1,400,000
Object Storage (VOD)S3 Standard + IA~100 PB$2,300,000
Object Storage (Live Segments)S3 Standard~5 PB$115,000
Misc (DNS, Monitoring, etc.)Various$50,000

Total Estimated Monthly Cost

~$8.5 million/month (~$102M/year)

Note: Transcoding is the largest cost driver (~50%). Platforms like Twitch optimize this by using custom hardware (AWS Inferentia), pre-encoding popular resolutions, and using hardware-accelerated encoders at scale. Actual costs at scale are likely 40–60% lower due to reserved instances, spot instances for transcoding, and volume discounts.

23. Interview Q&A (10+ Questions)

Q1: How would you handle a sudden spike in viewership when a popular streamer goes live (e.g., 500K viewers in 2 minutes)?

Answer: We use predictive autoscaling based on follow notification analytics — when a popular streamer schedules or starts a stream, we pre-warm CDN edges and spin up transcoding workers in advance. The CDN handles the viewer surge through request coalescing (one origin fetch per segment, millions of edge serves). Chat uses a fan-out architecture where message delivery is batched and distributed across gateway pools, preventing any single node from becoming a bottleneck. Redis cluster auto-scales with consistent hashing and read replicas.

Q2: How do you achieve sub-second chat latency at scale?

Answer: We use WebSockets (not long-polling) with connection multiplexing. Chat gateways use a pub/sub message bus (NATS or Kafka) where each room is a topic. Messages are batched in 50ms windows before broadcast. Gateway nodes are co-located with CDN PoPs for minimal network hop latency. The critical path is: Client → Gateway → Pub/Sub → Fanout Workers → Gateway → Client, typically completing in <200ms. We also use message compression (zstd) for bandwidth efficiency.

Q3: How would you design the clip creation system to avoid excessive storage costs?

Answer: Clips don't store separate video files. Instead, they store metadata (stream_id, start_offset, duration) and the playback system dynamically constructs a segment playlist pointing to the original stream's segments. This means clips consume zero additional storage for the video itself — only thumbnail images and metadata are stored. When the original VOD is archived to cheaper storage, clip playback continues to work because the segment URLs resolve against the archival storage tier.

Q4: How do you prevent DDoS attacks on the chat system?

Answer: We employ multiple layers: (1) Rate limiting at the connection level (max 20 messages/30s per user), (2) IP-based rate limiting at the gateway, (3) CAPTCHA challenges for suspicious connection patterns, (4) Geo-based filtering for known botnet regions, (5) Anycast routing to absorb volumetric attacks across multiple PoPs, (6) Automatic room locking if message rate exceeds threshold (e.g., 100K msg/sec). The pub/sub architecture naturally isolates rooms — an attack on one room doesn't affect others.

Q5: How do you handle stream key security and prevent unauthorized streaming?

Answer: Stream keys are generated as cryptographically random tokens with HMAC signatures. They're stored only as salted hashes — the plaintext is shown once upon creation. Keys are cached in Redis for fast validation during RTMP handshake (<10ms). If compromised, regeneration is instant (old key invalidated, new key cached immediately with a 60-second grace period for in-flight streams). Additionally, we bind stream keys to specific ingest servers to prevent key replay from different locations.

Q6: How would you design the Bits virtual currency system to prevent fraud?

Answer: Bits purchases go through fraud detection models before credit: (1) Payment method verification (3D Secure, device fingerprinting), (2) Velocity checks (max 100K Bits per user per day), (3) Account age verification (new accounts have lower limits), (4) Chargeback monitoring — if a purchase is reversed, the corresponding Bits are deducted and the user is flagged. The Bits balance is stored in Redis with write-through to PostgreSQL for durability. All balance mutations are idempotent and go through a transactional outbox pattern to prevent double-spending.

Q7: What happens when a transcoding GPU fails mid-stream?

Answer: The Transcoding Orchestrator monitors GPU health via heartbeats (every 5 seconds). If a heartbeat fails, the orchestrator immediately: (1) Signals the broadcaster's ingest server to redirect to a backup transcoding pipeline (cold standby), (2) The backup pipeline initializes in <5 seconds using pre-loaded FFmpeg templates, (3) Viewers experience a brief buffer (2–4 seconds) as their player switches to the backup rendition, (4) The failed GPU is marked unhealthy and removed from the rotation pool for investigation. We maintain a 10% hot standby capacity to handle random failures.

Q8: How do you handle the "thundering herd" when a mega streamer goes live?

Answer: Multiple strategies: (1) Pre-warming: CDN edges pre-fetch the playlist before the stream starts (based on stream scheduling API). (2) Request coalescing: CDN edges deduplicate concurrent segment requests. (3) Playlist caching: The master playlist is cached at CDN edge with a 1-second TTL, preventing origin overload. (4) Gradual rollout: The player uses jittered initial playlist fetch intervals (100–500ms random offset). (5) Circuit breaking: If origin load exceeds threshold, the CDN serves stale playlists with stale-while-revalidate.

Q9: How do you ensure VOD availability after a stream ends while minimizing costs?

Answer: We use a tiered storage lifecycle: (1) Hot tier (SSD-backed): First 24 hours — all VODs available for immediate playback. (2) Warm tier (S3 Standard): Days 2–30 — standard latency, lower cost. (3) Cold tier (S3 Infrequent Access): Days 31–90 — slightly higher retrieval latency, significant cost savings. (4) Archive tier (S3 Glacier): 90+ days — minutes to retrieve, minimal storage cost. VODs accessed during retrieval are transparently promoted to the warm tier. Popular VODs (high view counts) are kept in warm tier longer. Creators can pin important VODs to prevent archival.

Q10: How do you handle cross-region replication for chat messages?

Answer: Chat messages are region-local by design. Each region maintains its own chat infrastructure for active channels. When a viewer in EU watches a US streamer, their chat connection is routed to the US region where the channel's chat room lives (since all chat state must be in one place for consistency). We accept the ~50ms additional latency for cross-region chat as acceptable. For VOD chat replay, messages are replicated cross-region via Cassandra's built-in multi-datacenter replication with tunable consistency (LOCAL_QUORUM for writes, ONE for reads).

Q11: How would you design the recommendation system for "Recommended Channels"?

Answer: We use a hybrid approach: (1) Collaborative filtering: "Users who watched X also watched Y" — computed nightly via Spark on viewership data. (2) Content-based: Match channel tags, category, and language to user preferences. (3) Real-time signals: Boost channels that the user recently interacted with (followed, chatted in, clipped). (4) Popularity baseline: For cold-start users, fall back to global trending. The model is served via a feature store (Redis) with pre-computed candidate sets refreshed every 15 minutes.

Q12: How do you handle stream latency for interactive use cases (gaming with viewers)?

Answer: For ultra-low latency needs, we support WebRTC as an alternative to LL-HLS. WebRTC provides <1 second glass-to-glass latency but supports fewer simultaneous viewers (~10K per stream via SFU scaling). For most use cases, LL-HLS with 2–4 second latency is sufficient and scales to millions. The broadcaster can choose their preferred latency profile: Standard HLS (15–30s, best quality), LL-HLS (2–4s, good quality), or WebRTC (<1s, limited scale). Each profile has different CDN and infrastructure costs.

24. Full C# Implementation (300+ Lines)

Below is a complete C# implementation of the core streaming platform services, including the Ingest Server, Stream Manager, Chat Service, Viewer Counting, and Clip Service. This implementation demonstrates production-grade patterns including dependency injection, health checks, event-driven architecture, and distributed caching.

C# (.NET 8) — LiveStreamingPlatform.cs ~350 lines
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using System.Security.Cryptography;
using System.Text;
using System.Text.Json;

namespace LiveStreamingPlatform.Core
{
    // ─── ENUMS ───────────────────────────────────────────────
    public enum StreamStatus { Offline, Initializing, Live, Ending, Ended }
    public enum SubscriptionTier { Tier1 = 1, Tier2 = 2, Tier3 = 3 }
    public enum SubscriptionStatus { Active, Canceled, PastDue, Gifted }
    public enum PartnerStatus { None, Affiliate, Partner }
    public enum TranscodingStatus { Pending, Active, Failed, Completed }

    // ─── MODELS ──────────────────────────────────────────────
    public record StreamKey(
        string KeyHash,
        Guid ChannelId,
        DateTime CreatedAt,
        DateTime ExpiresAt
    );

    public class Channel
    {
        public Guid ChannelId { get; init; } = Guid.NewGuid();
        public Guid UserId { get; init; }
        public string DisplayName { get; init; } = string.Empty;
        public PartnerStatus PartnerStatus { get; set; } = PartnerStatus.None;
        public long FollowerCount { get; set; }
        public long SubscriberCount { get; set; }
        public DateTime CreatedAt { get; init; } = DateTime.UtcNow;
    }

    public class Stream
    {
        public Guid StreamId { get; init; } = Guid.NewGuid();
        public Guid ChannelId { get; init; }
        public string Title { get; set; } = string.Empty;
        public string Category { get; set; } = string.Empty;
        public string Language { get; set; } = "en";
        public StreamStatus Status { get; set; } = StreamStatus.Offline;
        public int ViewerCount { get; set; }
        public DateTime? StartedAt { get; set; }
        public DateTime? EndedAt { get; set; }
        public TranscodingStatus TranscodingStatus { get; set; }
        public Guid? IngestServerId { get; set; }
    }

    public class Clip
    {
        public Guid ClipId { get; init; } = Guid.NewGuid();
        public Guid StreamId { get; init; }
        public Guid ChannelId { get; init; }
        public Guid CreatorId { get; init; }
        public string Title { get; init; } = string.Empty;
        public int StartOffsetMs { get; init; }
        public int DurationMs { get; init; } = 30_000;
        public long ViewCount { get; set; }
        public string? VideoUrl { get; set; }
        public DateTime CreatedAt { get; init; } = DateTime.UtcNow;
    }

    public class Subscription
    {
        public Guid SubscriptionId { get; init; } = Guid.NewGuid();
        public Guid UserId { get; init; }
        public Guid ChannelId { get; init; }
        public SubscriptionTier Tier { get; init; } = SubscriptionTier.Tier1;
        public SubscriptionStatus Status { get; set; } = SubscriptionStatus.Active;
        public DateTime BillingCycleStart { get; set; } = DateTime.UtcNow;
        public DateTime BillingCycleEnd { get; set; }
        public Guid? GiftSenderId { get; init; }
    }

    public class ChatMessage
    {
        public Guid MessageId { get; init; } = Guid.NewGuid();
        public Guid ChannelId { get; init; }
        public Guid UserId { get; init; }
        public string Username { get; init; } = string.Empty;
        public string Message { get; init; } = string.Empty;
        public List<string> Badges { get; init; } = new();
        public string? Color { get; init; }
        public DateTime Timestamp { get; init; } = DateTime.UtcNow;
    }

    public record IngestServer(
        Guid ServerId,
        string Hostname,
        int Port,
        int CurrentStreams,
        int MaxStreams
    );

    public record ViewerHeartbeat(Guid ViewerId, Guid StreamId, DateTime Timestamp);

    // ─── INTERFACES ──────────────────────────────────────────
    public interface IStreamKeyManager
    {
        Task<string> GenerateKeyAsync(Guid channelId);
        Task<Guid?> ValidateKeyAsync(string streamKey);
        Task<bool> RevokeKeyAsync(Guid channelId);
    }

    public interface IViewerCounter
    {
        void RecordHeartbeat(Guid viewerId, Guid streamId);
        int GetViewerCount(Guid streamId);
        Task CleanupStaleViewersAsync(TimeSpan threshold);
    }

    public interface IChatService
    {
        Task<ChatMessage> SendMessageAsync(Guid channelId, Guid userId, string username, string message, List<string> badges);
        Task<List<ChatMessage>> GetRecentMessagesAsync(Guid channelId, int count = 50);
        Task<bool> ModerateMessageAsync(ChatMessage message);
    }

    public interface IClipService
    {
        Task<Clip> CreateClipAsync(Guid streamId, Guid creatorId, int offsetMs, string title);
        Task<List<Clip>> GetClipsForStreamAsync(Guid streamId, int limit = 20);
        Task<bool> IncrementClipViewsAsync(Guid clipId);
    }

    public interface ISubscriptionService
    {
        Task<Subscription> SubscribeAsync(Guid userId, Guid channelId, SubscriptionTier tier, Guid? giftSenderId = null);
        Task<bool> UnsubscribeAsync(Guid userId, Guid channelId);
        Task<int> GetSubscriberCountAsync(Guid channelId);
    }

    // ─── IMPLEMENTATIONS ─────────────────────────────────────

    /// <summary>
    /// Stream Key Manager — Generates and validates RTMP stream keys.
    /// Keys are stored as HMAC-SHA256 hashes with expiration.
    /// </summary>
    public class StreamKeyManager : IStreamKeyManager
    {
        private readonly ConcurrentDictionary<Guid, StreamKey> _keys = new();
        private readonly byte[] _hmacSecret = RandomNumberGenerator.GetBytes(32);

        public Task<string> GenerateKeyAsync(Guid channelId)
        {
            var rawBytes = RandomNumberGenerator.GetBytes(32);
            var rawKey = $"live_{Convert.ToHexString(rawBytes).ToLowerInvariant()}";

            using var hmac = new HMACSHA256(_hmacSecret);
            var hash = hmac.ComputeHash(Encoding.UTF8.GetBytes(rawKey));
            var keyHash = Convert.ToHexString(hash).ToLowerInvariant();

            var streamKey = new StreamKey(
                KeyHash: keyHash,
                ChannelId: channelId,
                CreatedAt: DateTime.UtcNow,
                ExpiresAt: DateTime.UtcNow.AddHours(24)
            );

            _keys.AddOrUpdate(channelId, streamKey, (_, _) => streamKey);
            return Task.FromResult(rawKey);
        }

        public Task<Guid?> ValidateKeyAsync(string streamKey)
        {
            using var hmac = new HMACSHA256(_hmacSecret);
            var hash = hmac.ComputeHash(Encoding.UTF8.GetBytes(streamKey));
            var keyHash = Convert.ToHexString(hash).ToLowerInvariant();

            var match = _keys.Values.FirstOrDefault(k =>
                k.KeyHash == keyHash && k.ExpiresAt > DateTime.UtcNow);

            return Task.FromResult(match?.ChannelId);
        }

        public Task<bool> RevokeKeyAsync(Guid channelId)
        {
            var removed = _keys.TryRemove(channelId, out _);
            return Task.FromResult(removed);
        }
    }

    /// <summary>
    /// Viewer Counter — Tracks concurrent viewers using heartbeats.
    /// Uses sliding window with 90-second expiry for stale viewer cleanup.
    /// </summary>
    public class ViewerCounter : IViewerCounter
    {
        private readonly ConcurrentDictionary<Guid, ConcurrentDictionary<Guid, DateTime>> _streamViewers = new();
        private readonly TimeSpan _heartbeatInterval = TimeSpan.FromSeconds(30);
        private readonly TimeSpan _staleThreshold = TimeSpan.FromSeconds(90);

        public void RecordHeartbeat(Guid viewerId, Guid streamId)
        {
            var viewers = _streamViewers.GetOrAdd(streamId, _ => new());
            viewers.AddOrUpdate(viewerId, DateTime.UtcNow, (_, _) => DateTime.UtcNow);
        }

        public int GetViewerCount(Guid streamId)
        {
            if (!_streamViewers.TryGetValue(streamId, out var viewers))
                return 0;

            var cutoff = DateTime.UtcNow - _staleThreshold;
            return viewers.Count(kvp => kvp.Value > cutoff);
        }

        public Task CleanupStaleViewersAsync(TimeSpan threshold)
        {
            var cutoff = DateTime.UtcNow - threshold;
            foreach (var (streamId, viewers) in _streamViewers)
            {
                var staleKeys = viewers
                    .Where(kvp => kvp.Value < cutoff)
                    .Select(kvp => kvp.Key)
                    .ToList();

                foreach (var key in staleKeys)
                    viewers.TryRemove(key, out _);

                if (viewers.IsEmpty)
                    _streamViewers.TryRemove(streamId, out _);
            }
            return Task.CompletedTask;
        }
    }

    /// <summary>
    /// Chat Service — Handles message send, history, and moderation.
    /// In production, this would use Redis Pub/Sub for fan-out and Cassandra for persistence.
    /// </summary>
    public class ChatService : IChatService
    {
        private readonly ConcurrentDictionary<Guid, List<ChatMessage>> _roomHistory = new();
        private readonly HashSet<string> _blockedWords = new(StringComparer.OrdinalIgnoreCase)
        {
            "spam", "scam", "hack", "free nitro", "click here"
        };
        private const int MaxHistoryPerRoom = 1000;

        public Task<ChatMessage> SendMessageAsync(
            Guid channelId, Guid userId, string username,
            string message, List<string> badges)
        {
            var chatMessage = new ChatMessage
            {
                ChannelId = channelId,
                UserId = userId,
                Username = username,
                Message = message,
                Badges = badges,
                Color = GenerateUserColor(userId)
            };

            var history = _roomHistory.GetOrAdd(channelId, _ => new());
            lock (history)
            {
                history.Add(chatMessage);
                if (history.Count > MaxHistoryPerRoom)
                    history.RemoveAt(0);
            }

            return Task.FromResult(chatMessage);
        }

        public Task<List<ChatMessage>> GetRecentMessagesAsync(Guid channelId, int count = 50)
        {
            if (!_roomHistory.TryGetValue(channelId, out var history))
                return Task.FromResult(new List<ChatMessage>());

            lock (history)
            {
                var recent = history
                    .Skip(Math.Max(0, history.Count - count))
                    .ToList();
                return Task.FromResult(recent);
            }
        }

        public Task<bool> ModerateMessageAsync(ChatMessage message)
        {
            var isBlocked = _blockedWords.Any(word =>
                message.Message.Contains(word, StringComparison.OrdinalIgnoreCase));

            return Task.FromResult(!isBlocked);
        }

        private static string GenerateUserColor(Guid userId)
        {
            var hash = userId.GetHashCode();
            var colors = new[] { "#FF6B6B", "#4ECDC4", "#45B7D1", "#96CEB4",
                                  "#FFEAA7", "#DDA0DD", "#98D8C8", "#F7DC6F" };
            return colors[Math.Abs(hash) % colors.Length];
        }
    }

    /// <summary>
    /// Clip Service — Creates clips from live/VOD streams and manages clip metadata.
    /// </summary>
    public class ClipService : IClipService
    {
        private readonly ConcurrentDictionary<Guid, Clip> _clips = new();
        private readonly ConcurrentDictionary<Guid, List<Guid>> _streamClips = new();

        public Task<Clip> CreateClipAsync(Guid streamId, Guid creatorId, int offsetMs, string title)
        {
            var clip = new Clip
            {
                StreamId = streamId,
                ChannelId = streamId,
                CreatorId = creatorId,
                Title = title,
                StartOffsetMs = offsetMs,
                DurationMs = Math.Min(Math.Max(offsetMs, 5_000), 60_000),
                VideoUrl = $"/clips/{Guid.NewGuid()}/playlist.m3u8"
            };

            _clips.TryAdd(clip.ClipId, clip);
            _streamClips.AddOrUpdate(streamId,
                new List<Guid> { clip.ClipId },
                (_, existing) => { existing.Add(clip.ClipId); return existing; });

            return Task.FromResult(clip);
        }

        public Task<List<Clip>> GetClipsForStreamAsync(Guid streamId, int limit = 20)
        {
            if (!_streamClips.TryGetValue(streamId, out var clipIds))
                return Task.FromResult(new List<Clip>());

            var clips = clipIds
                .Select(id => _clips.GetValueOrDefault(id))
                .Where(c => c != null)
                .OrderByDescending(c => c!.ViewCount)
                .Take(limit)
                .ToList()!;

            return Task.FromResult(clips);
        }

        public Task<bool> IncrementClipViewsAsync(Guid clipId)
        {
            if (!_clips.TryGetValue(clipId, out var clip))
                return Task.FromResult(false);

            var updated = clip with { ViewCount = clip.ViewCount + 1 };
            _clips.TryUpdate(clipId, updated, clip);
            return Task.FromResult(true);
        }
    }

    /// <summary>
    /// Subscription Service — Manages channel subscriptions with tier support.
    /// </summary>
    public class SubscriptionService : ISubscriptionService
    {
        private readonly ConcurrentDictionary<string, Subscription> _subscriptions = new();

        private string SubKey(Guid userId, Guid channelId) =>
            $"{userId}:{channelId}";

        public Task<Subscription> SubscribeAsync(
            Guid userId, Guid channelId, SubscriptionTier tier, Guid? giftSenderId = null)
        {
            var key = SubKey(userId, channelId);
            var subscription = new Subscription
            {
                UserId = userId,
                ChannelId = channelId,
                Tier = tier,
                GiftSenderId = giftSenderId,
                BillingCycleEnd = DateTime.UtcNow.AddDays(30)
            };

            _subscriptions.AddOrUpdate(key, subscription, (_, _) => subscription);
            return Task.FromResult(subscription);
        }

        public Task<bool> UnsubscribeAsync(Guid userId, Guid channelId)
        {
            var key = SubKey(userId, channelId);
            if (_subscriptions.TryGetValue(key, out var sub))
            {
                var canceled = sub with { Status = SubscriptionStatus.Canceled };
                _subscriptions.TryUpdate(key, canceled, sub);
                return Task.FromResult(true);
            }
            return Task.FromResult(false);
        }

        public Task<int> GetSubscriberCountAsync(Guid channelId)
        {
            var count = _subscriptions.Values
                .Count(s => s.ChannelId == channelId && s.Status == SubscriptionStatus.Active);
            return Task.FromResult(count);
        }
    }

    // ─── STREAM MANAGER (ORCHESTRATOR) ──────────────────────
    /// <summary>
    /// Stream Manager — Orchestrates the full lifecycle of a live stream.
    /// Handles start, ingest assignment, transcoding, viewer tracking, and end.
    /// </summary>
    public class StreamManager
    {
        private readonly IStreamKeyManager _keyManager;
        private readonly IViewerCounter _viewerCounter;
        private readonly IChatService _chatService;
        private readonly IClipService _clipService;
        private readonly ConcurrentDictionary<Guid, Stream> _activeStreams = new();
        private readonly List<IngestServer> _ingestServers;

        public StreamManager(
            IStreamKeyManager keyManager,
            IViewerCounter viewerCounter,
            IChatService chatService,
            IClipService clipService)
        {
            _keyManager = keyManager;
            _viewerCounter = viewerCounter;
            _chatService = chatService;
            _clipService = clipService;

            _ingestServers = Enumerable.Range(1, 5)
                .Select(i => new IngestServer(
                    Guid.NewGuid(), $"ingest-{i}.example.com", 1935, 0, 500))
                .ToList();
        }

        public async Task<Stream> StartStreamAsync(Guid channelId, string title, string category)
        {
            var server = _ingestServers
                .OrderBy(s => s.CurrentStreams)
                .FirstOrDefault() ?? throw new InvalidOperationException("No ingest servers available");

            var stream = new Stream
            {
                ChannelId = channelId,
                Title = title,
                Category = category,
                Status = StreamStatus.Initializing,
                IngestServerId = server.ServerId,
                StartedAt = DateTime.UtcNow,
                TranscodingStatus = TranscodingStatus.Pending
            };

            _activeStreams.TryAdd(stream.StreamId, stream);

            await Task.Delay(100);
            stream.Status = StreamStatus.Live;
            stream.TranscodingStatus = TranscodingStatus.Active;

            Console.WriteLine($"[StreamManager] Stream {stream.StreamId} started on {server.Hostname}");
            Console.WriteLine($"[StreamManager] LL-HLS URL: https://cdn.example.com/live/{stream.StreamId}/index.m3u8");
            return stream;
        }

        public async Task EndStreamAsync(Guid streamId)
        {
            if (!_activeStreams.TryGetValue(streamId, out var stream))
                throw new KeyNotFoundException($"Stream {streamId} not found");

            stream.Status = StreamStatus.Ending;
            stream.TranscodingStatus = TranscodingStatus.Completed;
            await Task.Delay(500);

            stream.Status = StreamStatus.Ended;
            stream.EndedAt = DateTime.UtcNow;

            Console.WriteLine($"[StreamManager] Stream {streamId} ended after " +
                $"{(stream.EndedAt - stream.StartedAt)?.TotalMinutes:F1} minutes");
        }

        public Stream? GetStream(Guid streamId) =>
            _activeStreams.GetValueOrDefault(streamId);

        public List<Stream> GetActiveStreams(string? category = null, string? language = null) =>
            _activeStreams.Values
                .Where(s => s.Status == StreamStatus.Live)
                .Where(s => category == null || s.Category.Equals(category, StringComparison.OrdinalIgnoreCase))
                .Where(s => language == null || s.Language.Equals(language, StringComparison.OrdinalIgnoreCase))
                .OrderByDescending(s => s.ViewerCount)
                .ToList();
    }

    // ─── MAIN PROGRAM — DEMONSTRATION ───────────────────────
    class Program
    {
        static async Task Main(string[] args)
        {
            Console.WriteLine("═══════════════════════════════════════════════════");
            Console.WriteLine("  Twitch-like Live Streaming Platform — Demo");
            Console.WriteLine("═══════════════════════════════════════════════════\n");

            var keyManager = new StreamKeyManager();
            var viewerCounter = new ViewerCounter();
            var chatService = new ChatService();
            var clipService = new ClipService();
            var subService = new SubscriptionService();
            var streamManager = new StreamManager(keyManager, viewerCounter, chatService, clipService);

            var channelId = Guid.NewGuid();
            var userId1 = Guid.NewGuid();
            var userId2 = Guid.NewGuid();

            // ── Stream Key Generation ──
            Console.WriteLine("▶ Generating stream key...");
            var streamKey = await keyManager.GenerateKeyAsync(channelId);
            Console.WriteLine($"  Stream Key: {streamKey[..20]}...");
            var validatedChannelId = await keyManager.ValidateKeyAsync(streamKey);
            Console.WriteLine($"  Key validated → Channel: {validatedChannelId}\n");

            // ── Start Stream ──
            Console.WriteLine("▶ Starting live stream...");
            var stream = await streamManager.StartStreamAsync(
                channelId, "Building Twitch in C# — LIVE!", "Software Development");
            Console.WriteLine($"  Stream Status: {stream.Status}");
            Console.WriteLine($"  Transcoding: {stream.TranscodingStatus}\n");

            // ── Simulate Viewers ──
            Console.WriteLine("▶ Simulating 10,000 concurrent viewers...");
            var random = new Random();
            for (int i = 0; i < 10_000; i++)
            {
                var viewerId = Guid.NewGuid();
                viewerCounter.RecordHeartbeat(viewerId, stream.StreamId);
            }
            var viewerCount = viewerCounter.GetViewerCount(stream.StreamId);
            Console.WriteLine($"  Current viewers: {viewerCount:N0}\n");

            // ── Chat Messages ──
            Console.WriteLine("▶ Sending chat messages...");
            var messages = new[]
            {
                "This is amazing!", "Love the stream!", "Can you explain that again?",
                "First time here, followed!", "GG!", "How did you do that?"
            };
            foreach (var msg in messages)
            {
                var chatMsg = await chatService.SendMessageAsync(
                    stream.ChannelId, userId1, "ViewerAlpha", msg, new List<string> { "subscriber" });
                var isClean = await chatService.ModerateMessageAsync(chatMsg);
                Console.WriteLine($"  [{chatMsg.Username}]: {chatMsg.Message} " +
                    $"(color: {chatMsg.Color}, clean: {isClean})");
            }
            Console.WriteLine();

            // ── Clips ──
            Console.WriteLine("▶ Creating clips...");
            for (int i = 0; i < 3; i++)
            {
                var clip = await clipService.CreateClipAsync(
                    stream.StreamId, userId1, random.Next(0, 300_000),
                    $"Highlight moment #{i + 1}");
                Console.WriteLine($"  Clip created: {clip.ClipId} — {clip.Title} " +
                    $"({clip.DurationMs / 1000}s)");
            }

            // ── Subscriptions ──
            Console.WriteLine("\n▶ Processing subscriptions...");
            var sub1 = await subService.SubscribeAsync(userId1, channelId, SubscriptionTier.Tier3);
            Console.WriteLine($"  User subscribed: Tier {sub1.Tier}, Status: {sub1.Status}");
            var sub2 = await subService.SubscribeAsync(userId2, channelId, SubscriptionTier.Tier1);
            Console.WriteLine($"  User subscribed: Tier {sub2.Tier}, Status: {sub2.Status}");
            var subCount = await subService.GetSubscriberCountAsync(channelId);
            Console.WriteLine($"  Total subscribers: {subCount}\n");

            // ── End Stream ──
            Console.WriteLine("▶ Ending stream...");
            await streamManager.EndStreamAsync(stream.StreamId);
            Console.WriteLine($"  Final Status: {stream.Status}");

            Console.WriteLine("\n═══════════════════════════════════════════════════");
            Console.WriteLine("  Demo complete — all subsystems operational.");
            Console.WriteLine("═══════════════════════════════════════════════════");
        }
    }
}
Key Patterns in the Implementation:
  • ConcurrentDictionary for thread-safe in-memory storage (production would use Redis)
  • Record types for immutable value objects (StreamKey, IngestServer)
  • Interface-based design enabling easy testing and swapping of implementations
  • HMAC-SHA256 for stream key hashing — production would use bcrypt/argon2
  • Sliding window cleanup for viewer counting with stale heartbeat removal
  • Event-driven architecture — each service is independently testable and deployable

26. Twitch Drops & Game Integration

Twitch Drops are in-game rewards that viewers earn by watching specific streams on the platform. This system bridges the gap between streaming and gameplay, creating a powerful engagement loop: game publishers drive viewership for their titles, streamers gain audiences, and viewers earn tangible in-game items. At peak events, Drops campaigns have driven over 30 million claimed rewards in a single week across the platform, making it one of Twitch's most impactful partner integrations.

The Drops system requires tight coordination between three separate platforms — the streaming infrastructure, the game publisher's backend, and the Twitch API — all while preventing fraud, handling millions of concurrent claim requests, and ensuring reward delivery guarantees. From a system design perspective, Drops represent a fascinating distributed transaction problem spanning multiple organizations and trust boundaries.

Drops System Architecture

sequenceDiagram participant Viewer as Viewer participant Twitch as Twitch Platform participant Drops as Drops Service participant Queue as Claim Queue participant Publisher as Game Publisher API participant Game as In-Game Inventory Viewer->>Twitch: Watches drop-enabled stream Twitch->>Drops: Heartbeat: viewer watching channel X Drops->>Drops: Check eligibility (game, duration) Note over Drops: Viewer must watch 2+ hours Drops->>Drops: Eligibility threshold met Drops->>Queue: Enqueue drop_claim event Queue->>Publisher: POST /drops/claim {user_id, drop_id, campaign_id} alt Publisher accepts Publisher->>Game: Grant item to player inventory Publisher-->>Queue: 200 OK {reward_id, item_data} Queue-->>Drops: Mark claim as fulfilled Drops-->>Twitch: Notify viewer: "Reward claimed!" Twitch-->>Viewer: In-app notification + chat badge else Publisher rejects (duplicate/invalid) Publisher-->>Queue: 409 Conflict Queue-->>Drops: Log rejection, no retry end

Drops Data Model

EntityKey FieldsPurpose
Campaigncampaign_id, game_id, publisher_id, start_time, end_time, drops[]Defines a Drops event (e.g., "Fortnite Chapter 5 Launch Drops")
Dropdrop_id, campaign_id, name, requirement_seconds, reward_idA single reward within a campaign with its watch-time requirement
Eligibilityviewer_id, channel_id, game_id, watched_seconds, statusTracks per-viewer progress toward earning a drop
Claimclaim_id, viewer_id, drop_id, claimed_at, fulfilled, publisher_responseRecords the claim transaction between Twitch and the publisher
DropConfigchannel_id, enabled_drops[], game_id, stream_tagsChannel-level configuration for which drops are active

Developer API & Integration Points

Game publishers integrate with the Drops system through a dedicated Drops Developer API that exposes several critical endpoints. The publisher must first register their game and create campaigns via POST /drops/developer/campaigns, specifying reward definitions, eligibility rules, and callback URLs. When a viewer becomes eligible for a reward, Twitch sends a webhook to the publisher's claim_callback_url with the viewer's platform identity and the reward details.

The publisher's backend validates the claim against their own player database, grants the in-game item, and responds with a fulfillment status. This request-response cycle must complete within 5 seconds or the claim is queued for retry with exponential backoff. Publishers can also poll the GET /drops/developer/campaigns/{id}/claims endpoint to batch-retrieve pending claims if they prefer polling over webhooks.

Drop Tracking & Watch-Time Accounting

Accurately tracking watch time is critical for Drops eligibility. The platform uses a distributed watch-time accumulator that processes viewer heartbeats and computes cumulative watch time per viewer per campaign. Each heartbeat contains the viewer's session token, the channel they are watching, and a timestamp. The Drops Service validates that the channel is actively streaming a game enrolled in a Drops campaign, then increments the viewer's elapsed watch time in a Redis sorted set.

To prevent manipulation, the system enforces several constraints: only one concurrent watch session counts per viewer (watching on multiple devices does not double-count), the streamer must be live (VOD replays do not qualify unless explicitly configured), and a minimum session continuity of 5 continuous minutes is required before watch time begins accumulating. The eligibility check runs asynchronously every 60 seconds, so there is a brief delay between reaching the threshold and receiving the claim notification. This eventual consistency is acceptable because Drops are not time-critical financial transactions.

Claim Flow & Fraud Prevention

The claim flow incorporates multiple layers of fraud prevention. Viewers cannot claim drops for channels that are not streaming the registered game — the system cross-references the stream's category against the campaign's game ID. Claim rate limiting prevents a single viewer from claiming more than 5 drops per hour. Publisher webhook signatures use HMAC-SHA256 to prevent tampering. All claim events are logged to an immutable audit trail in Cassandra for post-hoc fraud analysis. If the publisher's API is unreachable, claims are stored in a durable queue (Kafka) and retried with exponential backoff for up to 72 hours before being marked as failed and requiring manual intervention through the developer dashboard.

27. Twitch Ads & Monetization for Streamers

Advertising represents a significant revenue stream for both Twitch and its creators. The ad system must insert advertisements into live video streams without disrupting playback quality, handle server-side ad stitching so viewers cannot use browser ad blockers, support programmatic real-time bidding for ad inventory, and fairly compensate streamers based on ad impressions. At Twitch's scale, the ad system processes over billions of ad impressions per month, with peak throughput during major esports events and viral streams where a single channel may serve ads to over 100,000 concurrent viewers simultaneously.

Ad Insertion Architecture

graph TB subgraph "Ad Decision" ADX["Ad Exchange
(Real-Time Bidding)"] DSO["DSO Service
(Dual-Stream Overlay)"] ADREQ["Ad Request
Service"] end subgraph "Ad Pipeline" VAST["VAST Parser
(XML Response)"] SAAS["Server-Side Ad
Stitching (SSAI)"] SEGGEN["Ad Segment
Generator"] end subgraph "Delivery" CDN_AD["CDN Edge
(Ad Segments)"] PLAYER["Player Client
(Seamless Playback)"] end subgraph "Tracking" IMP["Impression
Tracker"] ATTR["Attribution
Service"] REV["Revenue
Calculator"] end ADREQ -->|viewer_context| ADX ADX -->|winning_bid + VAST| VAST VAST --> SAAS SAAS --> SEGGEN SEGGEN --> CDN_AD CDN_AD --> PLAYER PLAYER -->|viewable_impression| IMP IMP --> ATTR ATTR --> REV DSO --> SAAS

Ad Types & Revenue Model

Ad TypePlacementDurationRevenue Share (Creator)Revenue Share (Twitch)
Pre-RollBefore stream starts (viewer joins)30 sec55%45%
Mid-Roll (Manual)Streamer-triggered break30–180 sec55%45%
Mid-Roll (Automatic)Ad-scheduler inserts periodically30–60 sec55%45%
Display OverlayBanner below playerPersistentVariable (CPM)Variable (CPM)
Video BountyStreamer opts into specific campaignVaries100%0%

Server-Side Ad Stitching (SSAI)

Server-Side Ad Stitching is the cornerstone of Twitch's ad delivery system. Unlike client-side ad insertion where the player requests ads separately (and ad blockers can intercept them), SSAI stitches ad segments directly into the HLS/DASH manifest so the player treats ads as regular video segments. The flow works as follows:

  1. The Ad Decision Service receives a request containing the viewer's geo-location, device type, content category, and channel eligibility data.
  2. The service sends a real-time bid request to demand-side platforms (DSPs) via the ad exchange, which returns VAST (Video Ad Serving Template) XML responses containing the winning ad creative URL.
  3. The VAST Parser extracts the ad video URL, tracking pixels, and duration from the XML response.
  4. The SSAI Service downloads the ad creative, transcodes it into the same rendition profiles as the live stream (matching resolution, bitrate, and codec), and segments it into HLS parts matching the live segment duration.
  5. The ad segments are uploaded to the CDN alongside the live stream segments, and the viewer's manifest is updated to include ad segments in place of live content.
  6. The player seamlessly plays through the ad segments — from the player's perspective, there is no difference between live content and ad content.

Mid-Roll Ad Scheduling

Mid-roll ads are the primary revenue driver for streamers. The system supports both streamer-triggered and automatic scheduling modes. Streamers can manually insert ads via a dashboard button or API call: POST /api/v1/channels/:id/ads with a specified duration (30–180 seconds). Manual mid-rolls suppress pre-roll ads for the viewer for a proportional period — a 60-second mid-roll suppresses pre-rolls for 10 minutes, incentivizing streamers to run mid-rolls proactively.

The automatic ad scheduler uses a configurable interval (typically every 8 minutes for partnered channels) and inserts ads during natural pause points — scene transitions, loading screens, or low-chat-activity moments detected by the chat activity analyzer. The scheduler avoids inserting ads during critical moments (e.g., boss fights, tournament overtime) by monitoring game-state signals from the stream category and real-time chat sentiment analysis. If the streamer has configured "commercial break" mode, the scheduler skips that window entirely.

Ad Revenue Calculation Pipeline

Revenue calculation is a complex event-processing pipeline. Each ad impression generates an event containing the viewer ID, channel ID, ad creative ID, viewability metrics (was the tab in focus, was the player visible, what percentage of the ad was played), and the CPM (cost per thousand impressions) from the winning bid. These events flow through Apache Kafka into a Flink streaming job that computes rolling revenue aggregates in real-time. The revenue calculator applies the creator's revenue share percentage, accounts for ad blockers (impressions from blocked viewers are excluded), handles currency conversion for international campaigns, and computes net payouts after platform fees. Streamers can view their estimated ad earnings in near-real-time on the Creator Dashboard, with final settlement occurring weekly through the billing service.

28. Conclusion

Building a live streaming platform at Twitch's scale is one of the most challenging engineering problems in modern infrastructure. It requires deep expertise across multiple domains: real-time video processing, distributed systems, high-throughput messaging, global content delivery, and financial transaction processing.

The key architectural principles we've explored throughout this article are:

  1. Pipeline Architecture: Video flows through a clear pipeline — ingest → transcode → package → deliver — with each stage independently scalable and fault-tolerant.
  2. Separation of Concerns: Live video delivery, real-time chat, and metadata services are separate systems with different scaling characteristics, connected by event-driven messaging.
  3. Edge-First Design: CDN caching and edge computing minimize latency for the 99% of requests that are reads (video segment fetches).
  4. Graceful Degradation: The system is designed to degrade gracefully — chat can survive without video, video can play without chat, and discovery works even when some regions are unreachable.
  5. Cost Optimization: Tiered storage, GPU batching, and intelligent caching keep infrastructure costs manageable despite massive scale.

For system design interviews, the most important takeaways are the capacity estimation (bandwidth, storage, QPS), the transcoding pipeline design (why GPUs, how many renditions), the chat fan-out architecture (why not just broadcast to all), and the CDN caching strategy for live content. Understanding these core concepts will help you articulate a coherent, scalable design under interview time constraints.

As live streaming continues to grow — with use cases expanding from gaming to education, e-commerce ("live shopping"), social media, and enterprise webinars — the demand for engineers who understand these systems at a deep level will only increase. The patterns you've learned here apply not just to Twitch clones, but to any system requiring real-time media delivery at global scale.

Further Reading:
  • AWS re:Invent — "How Twitch Scales Live Streaming with AWS"
  • Apple — "Low-Latency HLS Specification (RFC 8216bis)"
  • Netflix Tech Blog — "Open Connect: Netflix's CDN Architecture"
  • Discord Engineering — "How Discord Stores Billions of Messages"
  • System Design Interview by Alex Xu — Chapters on URL Shortener, Chat Systems

Estimated word count: ~11,800+ words | 10 Mermaid diagrams | 6+ C# code blocks | 7+ HTML tables | 12 Interview Q&A