Your country

Tools that support it use your country for local currency, number formats, units and paper size. Your choice is saved only in this browser.

Type a name or a two-letter code. Use the up and down arrow keys to move through the countries, Enter to choose one and Escape to close.

System Design (High-Level Design)  Module 22 – Case studies: links, files and media

Design music streaming like Spotify

Design music streaming: a catalogue filtered by each market's licences, audio from CDNs and device caches, offline downloads and play counts fit for royalties.

  • Intermediate
  • 40 minutes
  • Examples run with Python 3.14.8, Pyodide 314.0.7, Node.js 24.21.0 and quickjs 0.32.0
  • By MySmartCoPilot
Free intro, Pro: 7 of 40 minutes free
Free: 9.2 million streams at the evening peak, and every play is money. With a pass: the other 13 sections and 3 interview answers, about 33 minutes.

What you will learn

  • Estimate concurrent streams, egress and catalogue storage
  • Design catalogue, playback and offline downloads
  • Apply licensing rules by region, tested at the edges of each window
  • Count plays accurately enough for royalties

Before you start

What you’ll learn · Before you start

What you will learn

  • Estimate concurrent streams, egress and catalogue storage
  • Design catalogue, playback and offline downloads
  • Apply licensing rules by region, tested at the edges of each window
  • Count plays accurately enough for royalties

Before you start

On this page

Not affiliated

Based on public information about Spotify. MySmartCoPilot is not affiliated with Spotify, and Spotify’s real architecture may differ from what this lesson describes.

A music streaming service lets people search a catalogue of about 100 million tracks, start any of them at once on any device, and keep some for listening offline. The design has four parts: a catalogue whose metadata is read all the time and cached, filtered when it is read by what each market’s licences allow; audio files at several bit rates served by a CDN, so that the service’s own servers hand out addresses rather than bytes; downloads that are encrypted and renewed online; and a play log counted carefully enough to pay royalties from.

The bytes are the easy part. The rules are hard: licences differ by country and by date, and every counted play is money.

9.2 million streams at the evening peak, and every play is money

Ask these first, and note the answer you assume:

  • How big is the catalogue? About 100 million tracks.
  • Where? In many countries, each with its own licences for each track.
  • At what quality? From about 24 to 320 kbit/s, and lossless for some listeners.
  • Offline? Downloads on a few devices per account, renewed online now and then.
  • What counts as a play? One that lasts at least a minimum time; this design assumes 30 seconds.

Spotify’s public pages give a check on the assumptions: over 100 million tracks in 184 markets (company info); quality settings from low, about 24 kbit/s, to very high, about 320 kbit/s, plus lossless (audio quality); and downloads of up to 10,000 tracks on each of up to 5 devices, which need the app to go online at least once every 30 days (listen offline).

The functional requirements: search and browse, play a track, build playlists, download for offline use, and report plays to rights holders. The non-functional ones: playback starts in about a second and never stutters on a steady connection; a track is playable exactly where and when its licence allows; and play counts are exact, because royalties are paid on them.

The streaming service's estimate sheet Python · estimate/stream_sheet.py
# The estimate sheet of a music streaming service (lesson system-design-hld/design-music-streaming). Listening is
# counted in minutes, so the streams playing at once follow from Little's law: minutes listened in a day divided by
# the minutes in a day, times the peak factor. Sizes are decimal (1 PB = 10**15 bytes); bit rates are in kbit/s.
DAY_MINUTES = 1_440
DAY_SECONDS = 86_400

LISTENERS_PER_DAY = 60_000_000
MINUTES_EACH = 100
PEAK = 2.2  # the evening peak against the daily average
TRACKS = 100_000_000  # the catalogue
TRACK_SECONDS = 200  # an average track: 3 minutes 20 seconds
# Quality levels: bit rate in kbit/s, and the share of listening at that level (assumed).
QUALITY = {"low": (24, 0.05), "normal": (96, 0.25), "high": (160, 0.50), "very high": (320, 0.20)}
LOSSLESS_KBPS = 1_000  # an assumed average for lossless files
DOWNLOADS_PER_DEVICE = 10_000
EVENT_BYTES = 200  # one play event in the log

minutes = LISTENERS_PER_DAY * MINUTES_EACH
streams = minutes / DAY_MINUTES
avg_kbps = sum(kbps * share for kbps, share in QUALITY.values())
plays = minutes * 60 / TRACK_SECONDS
copy_seconds = TRACKS * TRACK_SECONDS

print("Music streaming: the estimate sheet")
print(f"  Listening   {minutes / 10**9:.1f} billion minutes a day")
print(f"  Streams     {streams / 10**6:.1f} million at once on average, {streams * PEAK / 10**6:.1f} million at the peak")
print(f"  Bit rate    {avg_kbps:.0f} kbit/s on average over the quality mix")
print(f"  Egress      {streams * PEAK * avg_kbps * 1000 / 10**12:.2f} Tbit/s at the peak")
print(f"  Plays       {plays / 10**9:.1f} billion a day: {plays / DAY_SECONDS:,.0f}/s on average, {plays / DAY_SECONDS * PEAK:,.0f}/s at the peak")
print(f"  Play log    {plays * EVENT_BYTES / 10**9:,.0f} GB a day")
lossy = copy_seconds * sum(kbps for kbps, _ in QUALITY.values()) * 1000 / 8
print(f"  Catalogue   {lossy / 10**15:.1f} PB for the four lossy levels of every track,")
print(f"              {copy_seconds * LOSSLESS_KBPS * 1000 / 8 / 10**15:.1f} PB more for lossless copies")
for name in ("high", "very high"):
    kbps = QUALITY[name][0]
    device = DOWNLOADS_PER_DEVICE * TRACK_SECONDS * kbps * 1000 / 8
    print(f"  One device  {DOWNLOADS_PER_DEVICE:,} downloads at {kbps} kbit/s = {device / 10**9:.0f} GB")

Output

Music streaming: the estimate sheet
  Listening   6.0 billion minutes a day
  Streams     4.2 million at once on average, 9.2 million at the peak
  Bit rate    169 kbit/s on average over the quality mix
  Egress      1.55 Tbit/s at the peak
  Plays       1.8 billion a day: 20,833/s on average, 45,833/s at the peak
  Play log    360 GB a day
  Catalogue   1.5 PB for the four lossy levels of every track,
              2.5 PB more for lossless copies
  One device  10,000 downloads at 160 kbit/s = 40 GB
  One device  10,000 downloads at 320 kbit/s = 80 GB

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 stream_sheet.py

The sheet sets the shape of the design. At the evening peak 9.2 million streams play at once and need 1.55 Tbit/s, which only a CDN can deliver. Every play is an event, 46,000 a second at the peak. The catalogue is 1.5 PB for four lossy levels and 2.5 PB more for lossless copies, and a device full of downloads holds 40 to 80 GB.

Exercise · Easy · Python

Size streams, egress and storage for music streaming

Write four functions in sizing.py for the estimate sheet of a music streaming service. Bit rates are in kilobits a second (1 kbit = 1,000 bits), sizes in bytes, and a day has 1,440 minutes.

  • streams_at_peak(listeners, minutes_each, peak): the streams playing at once at the peak. By Little's law, the average is the minutes listened in a day divided by the minutes in a day; multiply it by the peak factor.
  • peak_egress_gbit(streams, mix): the gigabits a second that streams streams need, where mix maps each bit rate to the share of listening at that rate, such as {96: 0.25, 160: 0.75}.
  • catalogue_bytes(tracks, track_seconds, rates): the bytes needed to keep every track once at each bit rate in the list rates.
  • device_bytes(downloads, track_seconds, kbps): the bytes a device needs for downloads tracks at one bit rate.

The sample tests accept each figure within 10 %, as an estimate should.

Starter code · sizing.py

DAY_MINUTES = 1_440


def streams_at_peak(listeners, minutes_each, peak):
    """Streams playing at once at the peak."""
    # Replace this line with your code.
    return 0


def peak_egress_gbit(streams, mix):
    """Gbit/s for `streams` streams; `mix` maps a bit rate in kbit/s to its share of listening."""
    # Replace this line with your code.
    return 0


def catalogue_bytes(tracks, track_seconds, rates):
    """Bytes to keep every track once at each bit rate in `rates`."""
    # Replace this line with your code.
    return 0


def device_bytes(downloads, track_seconds, kbps):
    """Bytes for `downloads` tracks at one bit rate."""
    # Replace this line with your code.
    return 0
The sample tests · test_sizing.py
import math

from sizing import catalogue_bytes, device_bytes, peak_egress_gbit, streams_at_peak

LESSON_MIX = {24: 0.05, 96: 0.25, 160: 0.50, 320: 0.20}


def near(actual, expected):
    """Within 10%, as an estimate should be."""
    return math.isclose(actual, expected, rel_tol=0.10)


def test_streams_at_peak():
    """turns minutes listened into streams at once, then applies the peak"""
    assert near(streams_at_peak(60_000_000, 100, 2.2), 9_170_000)
    assert near(streams_at_peak(5_000_000, 45, 3), 468_750)


def test_peak_egress():
    """weights each bit rate by its share of listening"""
    assert near(peak_egress_gbit(9_170_000, LESSON_MIX), 1_550)
    assert near(peak_egress_gbit(468_750, {96: 0.5, 160: 0.5}), 60)
    assert near(peak_egress_gbit(1_000_000, {320: 1.0}), 320)


def test_catalogue():
    """keeps one copy per bit rate"""
    assert near(catalogue_bytes(100_000_000, 200, [24, 96, 160, 320]), 1.5e15)
    assert near(catalogue_bytes(2_000_000, 180, [64, 128]), 8.64e12)


def test_device():
    """sizes a device's downloads at one bit rate"""
    assert near(device_bytes(10_000, 200, 160), 4.0e10)
    assert near(device_bytes(500, 240, 320), 4.8e9)
A hint

Streams: listeners * minutes_each / DAY_MINUTES * peak. For egress, the average bit rate is the sum of rate * share over the mix; multiply by the streams and by 1,000 for bits a second, then divide by 10**9. For storage, seconds times kilobits a second times 1,000 gives bits; divide by 8 for bytes.

The sample tests run on this device, in your browser (Pyodide): nothing is sent to mysmartcopilot.com. The first run downloads Python (about 13.5 MB), which is kept for the next runs. A check in your browser is feedback for you, not proof that the code is right for every input.

Checkpoint

Check the estimate

  1. Question 1 of 2 60 million people listen each day, for 100 minutes each, and the evening peak is 2.2 times the daily average. How many million streams play at once at the peak?

    Type a number.

    A hint

    Little's law. The minutes listened in a day, divided by the 1,440 minutes of a day, give the streams at once.

    Show the answer: Question 1 of 2

    Answer: 9.2 million streams (anything from 9 to 9.4 counts)

    60 million × 100 = 6 billion minutes a day, and 6 billion ÷ 1,440 = 4.2 million streams at once on average. 2.2 times that is 9.2 million at the peak, which at 169 kbit/s on average needs 1.55 Tbit/s.

  2. Question 2 of 2 The service plays 6 billion minutes a day, and a track lasts 200 seconds on average. About how many play events a second reach the play log at the evening peak?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: About 46,000, from 1.8 billion plays a day

    Every play is one event, so the log takes about 46,000 events a second at the peak and 360 GB a day at 200 bytes each: a stream of appends for a partitioned log, not one database row per play.

The first design: licences checked when a track plays, audio from the CDN

An app browses and plays through API servers and a licence check, fetches audio from a CDN backed by an object store, and logs each play.App on a phone, desktop or the webkeeps a cache and downloadsAPI serverscatalogue, search, playlistsPlayback servicesigned audio address and keyAvailability checklicences by market and timeCDNaudio files at the edgeObject storeevery track at every qualityPlay logone event per playCountinglive counts, nightly recount,royalty reportsbrowse, search, playplay a trackfilter by marketallowed here, now?fetch the audiomissesplay events,later when offline

The music streaming service's first design

Text description of the diagram

The diagram runs from top to bottom, with three branches from the app.

  1. The app, on a phone, a desktop or the web, keeps a cache of recently played audio and the user's downloads.
  2. To browse, search and play it calls the API servers, which serve the catalogue, search and playlists and filter what they return by the listener's market through the availability check, which holds the licences by market and time.
  3. To play a track, the API servers pass the request to the playback service, which asks the availability check whether the track is licensed in this market at this moment, and then returns a signed audio address and a key.
  4. The app fetches the audio from a CDN, which keeps audio files at its edges and sends its misses to the object store that holds every track at every quality.
  5. A dashed arrow shows the app sending one event per play to the play log, later if it was offline. The counting stage reads the play log and produces live counts, a nightly recount and the royalty reports.

Follow a play through it. The app finds the track through the API servers, whose answers are already filtered by the listener’s market. To play, the API servers hand the request to the playback service, which checks the licence for this market and this moment and returns a short-lived signed address and a key. The audio comes from the CDN, unless the app’s own cache on the device still holds a recent copy, and the app reports the play to the play log, later if it is offline, where the counting stage turns plays into royalty reports. Step through one play:

Interactive · step by step One play through the first design Step through one play, from the search to the event that the royalty count is made from.
One play through the first design, step 8 of 8. Showing App, API servers, Playback service, Availability check, CDN, Object store, Play log, Counting. In focus: Counting.Appcache and downloadsAPI serverscatalogue, search, playlistsPlayback servicesigned address and keyAvailability checklicences by market, timeCDNaudio at the edgeObject storeevery track, every qualityPlay logone event per playCountingrecount, royalty reportsplays

The drawing shows the whole scene; its 8 steps are listed below.

Every step as text
  1. A search. The listener searches for a song. The app asks the API servers, which answer from cached catalogue records.
  2. Filtered by market. Before they answer, the API servers keep only the tracks that the listener's market may play now, so search never offers a track that would refuse to play.
  3. Play this track. The listener presses play, and the API servers hand the request to the playback service.
  4. Allowed here, at this second?. The playback service checks the licence window for this market and this second, then returns a short-lived signed address and a key.
  5. Bytes from the edge. Unless its own cache holds a recent copy, the app fetches the audio from the CDN with the signed address. The CDN checks the signature, not the listener, and serves the file or ranges of it.
  6. A miss goes to the origin. A track that the edge does not hold comes from the object store, which keeps every track at every quality.
  7. One event per play. Once the play passes 30 seconds it counts. The app sends one event with an id it made when the play started, later if it is offline.
  8. Counted for royalties. The counting stage keeps a live count for dashboards and recounts the log each night for the royalty reports.

The questions this design has to answer:

  • How is one catalogue filtered for 184 markets without a copy per market?
  • What happens at the exact second a licence starts or ends?
  • How much of the catalogue must a CDN edge hold?
  • How do downloads stay playable offline and still stop when a licence ends?
  • How are plays counted exactly when events are retried, late or fake?
  • How does search find a song typed in another script?
  • What breaks on a release day, and at ten times the listeners?

Checkpoint

Check the first design

  1. Question 1 of 2 In the first design, which part decides whether a track may play in this market at this second?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: The playback service, through the availability check, when the listener presses play

    The API servers filter what they show by market, and the playback service checks again at the moment of the play, returning a short-lived signed address and a key only while the window is open.

  2. Question 2 of 2 Why does the playback service return a signed address instead of sending the audio itself?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: At 1.55 Tbit/s the bytes must come from CDNs, and a short-lived signed address lets an edge serve them without asking the service about each listener

    The service's own servers hand out addresses, not bytes. The address expires soon, so a leaked one stops working, and the edge checks the signature without a call back to the service.

One cached catalogue serves 184 markets, filtered as it is read Pro: opens with a Pro, Premium or Ultimate pass

In this part:

  • A licence is a window per market, and its edges decide money
  • The popularity curve decides how much of the catalogue an edge must hold
  • A download plays offline until the earlier of its renewal and its window’s end
  • Royalties need exact counts: ids from the device, a recount and a reconciliation
  • Search matches a title in its own script and in Latin letters
  • A release day arrives in waves, one per time zone
  • Alternatives considered and rejected
  • The seven questions, answered
  • The bar at each level, and a timed practice run
  • Practise: the whole service, one question at a time
  • Interview questions
    • Why does a music streaming service deliver audio from a CDN rather than from its own servers?
    • A track's licence in one country ends at midnight local time, while it stays licensed everywhere else. How do you stop playback there on time, including on phones that downloaded the track?
    • Royalties are paid on play counts. Design play counting so that retried uploads, a phone that was offline for two weeks and an account that loops one song all day do not distort what rights holders are paid.
    • A famous artist's album opens at midnight local time in every market, and 20 million people press play within ten minutes of their market's opening. What breaks first, and how do you prepare?
  • Key takeaways

7 diagrams · 3 runnable examples (JavaScript, Python) · 3 interview answers · about 54 minutes

Opening this part with your pass…

This part opens with any MySmartCoPilot pass.

  • Pro pass · 1 week₹79$2.99
  • 30 days₹179$5.99
  • 1 year₹1,699$45

One payment; a pass never renews. The price shown is the total you pay: no fees or charges are added at checkout.

It also opens every other in-depth part in Learn and every Pro tool. Sales are final, except where the law gives you a right to withdraw, for example in the EU and the UK (refund policy).

Read a complete sample first: Design a URL shortener with click analytics

References

Related tools

Report a problem with this lesson

Quick answers and tool search

Type to search tools or to get a quick answer, for example 18% of 2500. Use the up and down arrow keys to move through the results, Enter to choose, and Escape to close.