How Netflix limits concurrent streams per account
Walk through the design challenge Netflix faced when limiting simultaneous screens per subscription plan, covering device identification, session tracking, distributed counting, and the race conditions that make this harder than it looks.
The Problem Statement
Interviewer: "Netflix lets you stream on a limited number of screens simultaneously, based on your plan. Standard gets 2 screens, Premium gets 4. How would you design the system that enforces this limit in real time, across millions of accounts worldwide?"
This question looks simple at first. Just count the active streams, right? But then you start thinking about it. What happens when two devices press Play within the same 100ms window? What defines an "active" stream when apps crash without sending a goodbye? How do you handle a user in Tokyo and another in New York on the same account, hitting different datacenters?
The hidden rubric: the interviewer wants to see whether you understand the check-then-act race condition, heartbeat-based session tracking, lease expiration, and distributed counting under concurrency. This is a concurrency control problem disguised as a feature question.
Estimating the Scale
Before I design anything, I need the numbers.
Accounts and streams:
- 250 million paid subscribers globally
- At peak (evening hours, staggered by timezone), roughly 15% are streaming: ~37.5 million concurrent streams
- Average account has 2.5 active profiles, but the stream limit is per account, not per profile
Play-start traffic:
- Users press Play roughly once per session. Average session length: 90 minutes.
- 37.5M concurrent streams / 90 min average = ~7,000 new play-start events per second at peak
- Spike during a major release (Stranger Things premiere): 3-5x normal, so ~25,000 play-starts/sec
Heartbeat traffic:
- Each active stream sends a heartbeat every 30 seconds to prove it is still alive
- 37.5M streams x (1 heartbeat / 30 seconds) = ~1.25 million heartbeats per second
- Each heartbeat: ~150 bytes (account ID, device ID, session token, timestamp)
- Bandwidth: 1.25M x 150 bytes = ~188 MB/s inbound. Significant but manageable.
Stream count checks:
- Every play-start triggers a "how many active streams does this account have?" check
- 7,000-25,000 reads/sec. Trivial compared to heartbeats.
The key insight from the math: the heartbeat path is the bottleneck, not the play-start path. You are designing for 1.25M writes/second (heartbeats) and only 25K reads/second (stream count checks). This is a write-heavy system.
Storage per stream session:
- Account ID (8 bytes) + device ID (32 bytes) + session token (36 bytes UUID) + timestamp (8 bytes) + plan limit (1 byte) = ~85 bytes per active session
- 37.5M active sessions x 85 bytes = ~3.2 GB. Fits comfortably in a Redis cluster.
My Approach
I break this into three core components:
- Session tracking with leased heartbeats: Every active stream periodically proves it is still alive. If the heartbeat stops (app crashed, network lost, user closed the laptop), the session expires automatically after a TTL window. No explicit "stop" event needed.
- Distributed stream count with race prevention: When a new device presses Play, atomically check the count and add the new session in one indivisible operation, preventing the classic check-then-act race condition.
- Device identification and session deduplication: Distinguish between "same device reopening the app" and "new device starting a separate stream." Prevent device spoofing while avoiding false rejections for legitimate users.
The design philosophy: prefer self-healing over explicit cleanup. Streams that crash should not permanently consume a slot. Heartbeat expiry gives you automatic garbage collection of dead sessions without relying on the client to send a clean teardown.
The Architecture
Here is how the flow works step by step:
- Device presses Play: The client sends a play-start request with its device ID and authentication token to the API Gateway.
- Gateway authenticates: Validates the JWT, extracts the account ID, and forwards to the Stream Control Service.
- Atomic check: The Play Start Handler calls a Redis Lua script that atomically counts active sessions for this account (HLEN) and adds the new session (HSET) only if the count is below the plan limit.
- Allowed or rejected: If the count was below the limit, the session is added and the device receives a session token. If at the limit, the device gets a 403 with a "too many streams" message.
- Heartbeats keep the session alive: Every 30 seconds, the device sends a heartbeat that refreshes the TTL on its session entry. If heartbeats stop, the entry expires in 60 seconds.
- Explicit stop is best-effort: When a user stops playback, the client sends a stop event to immediately free the slot. But the system does not depend on it. Heartbeat expiry handles cleanup.
Session Tracking with Leased Heartbeats
The core challenge: how do you know a stream is still active? You cannot trust the client to tell you when it stops, because apps crash, phones lose power, and users close laptop lids without pausing.
The answer is a lease-based approach. Every stream has a lease that it must periodically renew by sending a heartbeat. If the lease expires, the stream is considered dead.
The heartbeat interval (30s) is half the TTL (60s). This gives each device two chances to get a heartbeat through before the lease expires. If one heartbeat is lost to a network blip, the next one arrives before the TTL deadline.
A common interview mistake is setting the heartbeat interval equal to the TTL. If the interval is 60 seconds and the TTL is 60 seconds, a single delayed heartbeat (network jitter, GC pause on the client) causes the session to expire. Always set the heartbeat interval to half the TTL or less, giving the client at least two attempts before expiry.
Distributed Stream Count with Race Prevention
Continue Reading with Premium
Unlock this article and every other in-depth system design guide on the platform with NotesFromSDE Premium.