Guide · System design
Music Streaming System Design: Build an App Like Spotify
27 min readUpdated By Saket Bhatnagar
Researched and drafted with AI, reviewed and edited by Saket Bhatnagar.
In short
- Music streaming system design is the plan behind an app that lets people search songs, start them in about a second and play downloads offline. It has three parts: a catalogue, a playback path and a download path.
- Keep two kinds of data apart. Metadata (title, artist, rights) lives in a database and a search index. Audio files live in object storage, a cheap store for big files, served through a CDN (nearby copy servers).
- Playback starts fast because the app downloads only the first few seconds, plays them at once, and fetches the start of the next song before the current one ends.
- Offline downloads are encrypted files plus a license, a small record holding the key, tied to one device. The app must go online now and then to renew it.
- Beginners can stop after the section on counting plays. Engineers then get a playlist that maps a song to byte ranges, tested with curl (a command-line web tool), an expiring signed link, retry-proof play counts and a wound-back-clock check.
What goes wrong when a music app outgrows one server?
Songs start slowly, search misses what people type, and downloaded songs get copied. A music app built on one server does every job in one place, and a busy evening exposes each weak spot at once.
Picture a common setup. A small team runs a music app for Bengali and Hindi songs, with about 3 lakh (300,000) songs from small labels, film studios and folk artists. The setup is made up, but the problems are real.
The first version was one server, one database and a folder of MP3 files. It worked well for 5,000 listeners. Then the app launched a Durga Puja playlist, and 60,000 people opened it within one evening hour (made up).
- Songs took 8 to 10 seconds to start (made up), because the single server sent every byte of every song itself.
- A search typed in English letters, like amar shonar bangla, found nothing, because the database held only Bengali script titles.
- Offline downloads were plain MP3 files. A listener posted on a forum how to copy them to a pen drive.
Each failure maps to one part of the design. This guide rebuilds the app in three parts: the catalogue, playback and offline downloads. Then it adds the play counting that pays the artists.
What is music streaming system design?
Music streaming system design is planning the servers, storage and app logic behind a music app. It lets many people search songs, start any track within about a second, and keep listening offline.
Four words come up again and again, so here they are in plain terms.
Metadata. Metadata is data about a song, not the sound itself. Title, artist, album, language, length and cover image link are all metadata. At about 3 KB per song (made up), 3 lakh songs need under 1 GB.
Object storage. Object storage is a service that keeps large files, called objects, under a name and hands them back when asked. It is cheap per gigabyte, so the team keeps its audio files there.
CDN. A CDN, or content delivery network, is a set of servers spread across many cities that keep copies of popular files. A listener in Siliguri gets bytes from a nearby server, not from the team's main data centre.
Bitrate. Bitrate is how many bits of audio data are used for each second of sound, written as kbps (kilobits per second). A higher bitrate can sound better but needs a faster network and more storage.
What does a music app like Spotify need to do?
A Spotify-like app must let users search and browse songs, play them with almost no wait, make playlists, and download songs for offline use. It must also count plays correctly, because artists are paid from those counts.
Requirements come in two kinds. Functional requirements say what the app does. Non-functional requirements say how well it must do it: how fast, for how many users, and how safely.
- Functional: search by title, artist or a lyrics line, in Bengali script, Hindi script or English letters.
- Functional: play, pause, skip and seek, which means jumping to any point in a song.
- Functional: playlists, likes and offline downloads for paying users.
- Non-functional: a song should start in under one second for most taps, even on a 4G phone.
- Non-functional: downloaded songs must not play outside the app, and must stop within 30 days of a subscription ending.
- Non-functional: the catalogue changes slowly, but playback traffic spikes hard on festival evenings.
Now a quick estimate, the kind you do on a whiteboard. An average song here is 4 minutes long, which is 240 seconds. The team stores each song at four bitrates, so phones on any network have something to play.
| Quality | Bitrate | Size per song | Size for 3 lakh songs |
|---|---|---|---|
| Data saver | 48 kbps | about 1.44 MB | about 0.43 TB |
| Normal | 96 kbps | about 2.88 MB | about 0.86 TB |
| High | 160 kbps | about 4.8 MB | about 1.44 TB |
| Very high | 320 kbps | about 9.6 MB | about 2.88 TB |
| All four | 624 kbps in total | about 18.7 MB | about 5.6 TB |
The maths is simple. At 160 kbps, one second of audio is 160 kilobits, or 20 kilobytes. Multiply by 240 seconds and you get 4.8 MB for one song.
The surprise is how small the catalogue is. All the audio fits in under 6 TB. The real cost is bandwidth. If all 60,000 people listened at 160 kbps at the same moment, the app would send 9.6 gigabits every second. One hour of that is about 4.3 TB, three times the whole 160 kbps catalogue.
How do you store and search a music catalogue?
You store the catalogue in two places: song details go in a normal database, and the audio files go in object storage. A separate search index, built from the database, answers what users type in the search box.
Think of a 1990s cassette shop in Gariahat. The owner keeps a fat register listing every album, singer and song. The cassettes sit in boxes at the back. You read the register first, then he fetches the tape.
The mapping is direct. The register is the metadata database. The boxes at the back are object storage. Nobody carries all the boxes to the counter just to answer which albums a singer has made.
Where the picture breaks: a shop has one copy of each tape, and only one customer can hold it. Object storage and the CDN keep many copies, so thousands of people can play the same song at once.
The metadata database
The team uses a relational database, one that keeps data in tables with rows and columns. It has tables for songs, artists, albums and the links between them. One song can have several singers, and one album has many songs.
Each song row also holds the names of its audio objects, one per bitrate. It also holds licensing rights: which countries the song may play in, and whether it may be downloaded.
The catalogue is read far more often than it changes, because it changes only when labels upload new songs. So the team puts a cache in front of the database. A cache is a fast in-memory store that keeps recent answers.
Search in three scripts
A search index is a separate store built only for fast text lookups. It works like the index at the back of a book. Each word points to the songs that contain it.
The team's problem was scripts. People type the same song in Bengali script, in Hindi script, or in English letters with their own spellings. So for every title, the index stores extra spellings: shonar, sonar and sona all point to the same song.
The index also allows small typos and ranks results by popularity. When a label adds a song, the database changes first. In this design, a small worker then copies the change into the index within about a minute.
Here is the whole architecture. Start at the listener's phone. Notice that audio never passes through the team's API servers, the servers that answer the app's requests. Audio goes from the CDN straight to the phone.
The music app's architecture
Drag to exploreSmall metadata goes through the API. Big audio files go from the CDN straight to the phone, and play events go to a queue.
Diagram connections:
- Music app to API layer (Search, browse, play)
- API layer to Catalogue service
- Catalogue service to Metadata database (Read song details)
- Metadata database to Search index (Worker copies changes)
- Catalogue service to Search index (Run searches)
- API layer to Playback service (Ask to play)
- Music app to CDN edge servers (Fetch audio pieces)
- CDN edge servers to Audio storage (On a cache miss)
- API layer to Play events queue (Send play events)
- Music app: Android and iPhone, with an encrypted local cache
- API layer: Checks login, routes each request
- Catalogue service: Songs, artists, albums, playlists
- Metadata database: Song details and licensing rights
- Search index: Titles in three scripts, typo tolerant
- Playback service: Checks plan, hands out signed links
- CDN edge servers: Keeps popular audio near listeners
- Audio storage: Every song at four bitrates
- Play events queue: Feeds play counts and royalties
How does a song start playing in about a second?
A song starts fast because the app asks a nearby CDN server for only the first few seconds of audio, and starts playing as soon as those bytes arrive. The rest of the song downloads while you listen.
Spotify's own 2010 paper describes the same move. When a track was not cached, its computer client asked a server for about 15 seconds of music first. The rest came later.
The steps behind one tap
Follow the boxes from the tap to the speaker. Each box says what happens, and the small line says who talks to whom. Notice the two paths after the app asks the CDN for the first piece.
What happens when you tap play
Drag to exploreThe playback service decides whether you may play. The CDN does the heavy lifting, and only a cache miss reaches audio storage.
Diagram connections:
- Tap play to Check plan and rights
- Check plan and rights to Return a signed link
- Return a signed link to Ask for the first piece
- Ask for the first piece to Start playing (Cache hit)
- Ask for the first piece to Fetch from storage (Cache miss)
- Fetch from storage to Start playing
- Start playing to Prefetch the next song (Near the end of the song)
- Tap play: App → Playback service
- Check plan and rights: Playback service → Metadata database
- Return a signed link: Playback service → App
- Ask for the first piece: App → CDN edge
- Fetch from storage: CDN edge → Audio storage
- Start playing: CDN edge → App
- Prefetch the next song: App → CDN edge
The signed link is a web address that carries an expiry time and a signature, a code only someone holding the team's secret key can make. The CDN checks both before sending a byte. A link copied to a forum stops working once it expires, here after five minutes.
The app asks for pieces of the file with HTTP range requests. A range request names the bytes it wants, such as the first 500 bytes. A server that supports ranges replies with status 206, Partial Content, and sends only those bytes.
Range requests also make seeking cheap. When you drag the slider to the last chorus, the app fetches only the piece that holds that moment. It does not need to download the middle part you skipped.
Prefetching and the local cache
Prefetching means fetching the next song before anyone asks for it. In Spotify's 2010 measurements, 61% of plays came in a predictable order, because the last song ended or the user pressed next.
That paper describes the 2010 Windows and Mac clients, not today's apps; its phone clients streamed only from servers. The computer clients started downloading the next track from other listeners' computers when 30 seconds or less remained. At 10 seconds or less, they fetched the beginning from a server if needed.
The same paper reports a median start delay of 265 ms over one measurement week in March 2010. A bug once turned prefetching off for a few weeks. During one such week the median was 390 ms.
The app also keeps a local cache of recently played songs, stored encrypted on the phone. In that paper's measurements, 55.4% of the music data clients used came from their local cache, and only 8.8% from Spotify's servers. The other 35.8% came from listeners' computers sharing pieces.
The team skips that sharing network and uses a CDN instead. It keeps the encrypted local cache, because replays of favourite songs then need no network at all.
How does a music app handle slow mobile networks?
A music app handles slow networks by switching to a lower bitrate when the connection weakens, and back up when it improves. This is called adaptive bitrate streaming. It trades a little sound quality for fewer pauses.
HTTP Live Streaming, or HLS, is a common way to do this. It is described in RFC 8216, part of the numbered RFC series where internet standards are also published. RFC 8216 is an informational, independent submission from August 2017, not a formal standard. As of 19 October 2026, a second edition that would replace it is with the RFC Editor, the office that publishes RFCs, as a draft. The server cuts each song into short media segments listed in a playlist file.
A master playlist lists versions of the same song at different bitrates, called variant streams. RFC 8216 says clients should switch between variant streams to adapt to network conditions. It also requires matching content in every variant to have matching timestamps.
Matching timestamps are what let the app change bitrate between two segments without a gap. Segment 12 at 96 kbps and segment 13 at 160 kbps line up exactly.
The app picks a bitrate before each segment. The rule looks at two numbers: the measured download speed, and the buffer, which is how many seconds of audio are already downloaded and waiting.
BITRATES_KBPS = [48, 96, 160, 320]
def pick_bitrate(measured_kbps, buffer_seconds, on_wifi):
budget = measured_kbps * 0.7
if buffer_seconds < 5:
budget = budget / 2
best = BITRATES_KBPS[0]
for rate in BITRATES_KBPS:
if rate > budget:
break
if rate == 320 and not on_wifi:
break
best = rate
return best
print(pick_bitrate(1000, 20, False)) # 160
print(pick_bitrate(1000, 20, True)) # 320
print(pick_bitrate(300, 20, False)) # 160
print(pick_bitrate(300, 3, False)) # 96
print(pick_bitrate(100, 3, False)) # 48Three lines matter. The 0.7 keeps a safety margin, because measured speed on a moving bus jumps around. Halving the budget when the buffer is under 5 seconds makes a pause less likely just before it would happen.
The third rule is about money, not speed. The team keeps 320 kbps for Wi-Fi only, so a listener's mobile data pack is not spent on the largest step by default.
What just happened? On a 1,000 kbps 4G link the rule picked 160, not 320, because of the Wi-Fi rule. At 300 kbps with only 3 seconds buffered, the budget fell to 105 kbps, so it picked 96.
The last call shows the honest limit. At 100 kbps with a small buffer, the budget is 35 kbps, below even the lowest version. The rule still returns 48, and the listener may hear a pause.
How do offline downloads work in a music app?
Offline downloads work by saving encrypted audio files on the phone, plus a license that holds the key to open them. The license is tied to one device and one account, and it stops working unless the app checks in online.
The parts of an offline download
- Encrypted file. The audio is scrambled with a key, so the file on disk is useless without it. The team uses the same files it streams, already encrypted in storage.
- License. A small record from the team's license server holding the key, the device id, the account and an expiry date.
- Secure storage. The phone keeps the license in a protected area that other apps cannot read.
- Check-in. When online, the app renews licenses and deletes any whose subscription has ended.
Big services publish their own limits. As of 19 October 2026, Spotify's help page says Premium users can download up to 10,000 tracks on each of up to 5 devices. It also says you must go online at least once every 30 days to keep downloads.
The team copies that shape with smaller numbers. Each account may download songs on up to 3 devices. Every license works for 30 days after the last online check.
Follow the diagram from the download tap. Notice that the file and the license travel separately. The question box runs every time a song plays without a network.
How an offline download works
Drag to exploreThe file is useless without the license, and the license stops working without a check-in every 30 days.
Diagram connections:
- Tap download to Download service
- Download service to Encrypted audio files (Allowed)
- Download service to License server (Issue a license)
- Encrypted audio files to Saved on the phone
- License server to Saved on the phone
- Saved on the phone to Checked in within 30 days? (Later, with no network)
- Checked in within 30 days? to Decrypt and play (Yes)
- Checked in within 30 days? to Ask to go online (No)
- Tap download: On a playlist, over Wi-Fi
- Download service: Checks plan and the 3-device limit
- Encrypted audio files: Same files as streaming, from the CDN
- License server: Sends key, device id and expiry
- Saved on the phone: Files on disk, license in secure storage
- Checked in within 30 days?: Asked on every offline play
- Decrypt and play: Key stays inside the app
- Ask to go online: Renews or removes the license
The 30-day check is where a cancelled plan gets noticed. Between check-ins, the phone only knows the last answer it was given. The engineer section below shows this check in code, including a phone whose clock was wound back.
How do music apps stop downloaded songs from being copied?
Music apps stop casual copying with DRM, short for digital rights management: songs are encrypted, and only protected decryption code gets the key. It cannot stop every determined attacker, but it stops the pen drive trick.
Phones and browsers already provide DRM building blocks, so the team does not invent its own. On the web, the W3C's Encrypted Media Extensions (EME) give pages a common way to play encrypted media.
EME calls the protected part a Content Decryption Module, the client component that does the decryption for a key system. A page can open a key session of type temporary, the default, or persistent-license.
A persistent-license session needs a key system that can store state on the device. A page can later reload a stored session by its session id. As of 19 October 2026, EME 2 is a W3C Working Draft dated 13 October 2026; the last Recommendation is from 2017.
HLS has a simpler option. RFC 8216 lets a playlist mark segments as encrypted with AES-128, a standard cipher, and gives a URI that says how to get the key. Anyone who can fetch that key can decrypt the segments, so the key server must check who is asking.
- Encrypt every audio file in storage, not just the downloaded ones.
- Keep keys on a separate license server, never inside the audio files.
- Give each license a device id and an expiry date.
- Accept that someone can always record the speaker output, and aim to stop easy copying, not all copying.
How does a music app count plays for royalties?
A music app counts plays by sending a small play event from the phone to a queue, which separate workers read to build daily counts per song. Labels and artists are paid from those counts.
A play event is a short message: who played which song, when, for how long, and whether it was online or offline. As of 19 October 2026, Spotify for Artists counts a stream after at least 30 seconds of listening. The team uses the same 30-second rule.
The events go into a message queue, a service that holds messages until workers are ready for them. If the counting system is slow on a festival night, events wait in the queue. The music keeps playing.
Offline plays are stored on the phone and sent when it next goes online. Spotify's artist help page says the same: streams are counted when the listener goes online. Each event carries a unique id, so a batch sent twice is counted once.
The app records the play
After 30 seconds, the app writes an event with an id, the song, the user and the time. Offline, it saves the event in a small local list.
The API puts it on the queue
The API layer accepts the event and adds it to the play events queue. It replies at once and does no counting itself.
A worker removes duplicates
A counting worker reads events and skips any id it has already saved, so retries after a weak network do not count twice.
Counts roll up each day
The worker adds plays per song per day into a reporting database. The monthly royalty report reads only from there.
How do HLS playlists point at byte ranges of one audio file?
An HLS media playlist can list each segment as a byte range inside one big file, using the EXT-X-BYTERANGE tag. The team then stores one file per bitrate instead of dozens of small segment files per song.
RFC 8216 writes the tag as a length in bytes, then an @ sign and a start offset. Using the tag needs playlist version 4 or later. The app turns each line into an ordinary HTTP range request.
You can build this yourself. The commands below use ffmpeg, a free media tool, to make a 240-second test tone with a little noise. Then they encode it at 160 kbps as HLS with one file and print the playlist's first lines.
ffmpeg -f lavfi -i "sine=frequency=440:duration=240" \
-f lavfi -i "anoisesrc=d=240:a=0.05" -filter_complex amix=inputs=2 \
-ar 44100 -ac 2 song.wav
ffmpeg -i song.wav -c:a aac -b:a 160k -f hls -hls_time 6 \
-hls_playlist_type vod -hls_flags single_file \
-hls_segment_filename song_160k.ts song_160k.m3u8
head -9 song_160k.m3u8
# #EXTM3U
# #EXT-X-VERSION:4
# #EXT-X-TARGETDURATION:6
# #EXT-X-MEDIA-SEQUENCE:0
# #EXT-X-PLAYLIST-TYPE:VOD
# #EXTINF:6.013967,
# #EXT-X-BYTERANGE:130472@0
# song_160k.ts
# #EXTINF:5.990756,Read the playlist from the top. Each segment's length, rounded to whole seconds, is at most 6, the target duration; the first lasts 6.013967 seconds. The first segment is 130,472 bytes starting at byte 0, and each later one starts where the last ended. Another ffmpeg version may give slightly different byte counts.
Now serve the folder with any web server that supports ranges, and ask for segment two the way a player would. In our run, segment two was 129,908 bytes long and started at byte 130,472.
curl -s -r 130472-260379 -o seg2.bin -D - http://localhost:8080/song_160k.ts
# HTTP/1.1 206 Partial Content
# Content-Range: bytes 130472-260379/5206472
# Content-Length: 129908Three things confirm it worked. The status is 206, not 200. Content-Range repeats your bytes and the full file size. Content-Length matches the segment length from the playlist exactly.
We ran this with ffmpeg 6.1.1, the http-server 14.1.1 package on Node.js and curl 8.5.0. Asking for a range past the end of the file returned 416, which means the range cannot be satisfied.
Expect the file to be bigger than the bitrate maths
The whiteboard said 4.8 MB for a 4-minute song at 160 kbps. Our MPEG-TS file came out at 5,206,472 bytes, about 8.5% more. MPEG-TS is a container format, the wrapper that holds audio frames plus timing data.
The audio itself matched the plan. The same AAC encode (AAC is a common audio format) in fMP4, another container HLS supports, held about 4.81 MB of audio in a 4.86 MB file. That is about 1% overhead.
The MPEG-TS file added a 7-byte header to each of its 10,337 audio frames, plus its own packet headers and padding. Together they added about 0.4 MB to the same audio.
These sizes come from one synthetic test track, so treat them as an example, not a rule. The lesson holds, though: size storage and bandwidth from real encoded files, not from the bitrate alone.
How seeking finds the right bytes
With a playlist, seeking is simple addition. The app adds segment durations until it reaches the chosen moment, then requests that segment's byte range.
A single file without a playlist needs an index of times to byte offsets. Spotify's 2010 paper says Ogg Vorbis, its format then, offered limited seeking support for streaming. So Spotify added a custom header to every file.
How does a signed audio link expire?
A signed link carries an expiry time and a signature made from the path and that time with a secret key. The CDN recomputes the signature and refuses the request if it does not match or the time has passed.
The signature here is an HMAC, a code made by hashing the message together with a secret key. CloudFront, for one, follows this shape: its signed URLs carry Expires, Signature and Key-Pair-Id parameters. The sketch below uses one shared secret instead of a public key pair, to keep the idea visible.
import hashlib
import hmac
SECRET = b"replace-with-a-long-random-secret"
def sign(path, expires):
message = f"{path}:{expires}".encode()
return hmac.new(SECRET, message, hashlib.sha256).hexdigest()
def make_link(path, now, ttl_seconds=300):
expires = now + ttl_seconds
return path, expires, sign(path, expires)
def cdn_allows(path, expires, signature, now):
if now > expires:
return "expired"
if not hmac.compare_digest(signature, sign(path, expires)):
return "bad signature"
return "ok"
now = 1_760_000_000
path, expires, signature = make_link("/audio/song_160k.ts", now)
print(cdn_allows(path, expires, signature, now + 10)) # ok
print(cdn_allows(path, expires, signature, now + 301)) # expired
print(cdn_allows("/audio/song_320k.ts", expires, signature, now)) # bad signature
print(cdn_allows(path, expires + 3600, signature, now)) # bad signatureWhat just happened? The same link worked at 10 seconds and failed at 301 seconds. Swapping the file for the 320 kbps version broke the signature, and so did pushing the expiry an hour later.
Two details catch teams out. Python's compare_digest avoids stopping at the first wrong character, so response times do not leak how much of a guessed signature was right. And the phone's clock never decides expiry here; the CDN's clock does.
How do you stop retried play events from being counted twice?
Make saving an event idempotent, meaning that doing it twice has the same effect as doing it once. Store each event under its unique id, and ignore an id you already have.
The demo below uses SQLite, a small database built into Python. INSERT OR IGNORE skips any row whose event_id already exists, because event_id is the primary key, the column that must be unique.
import sqlite3
db = sqlite3.connect(":memory:")
db.execute("CREATE TABLE plays (event_id TEXT PRIMARY KEY, song_id TEXT, day TEXT)")
def save_batch(events):
db.executemany("INSERT OR IGNORE INTO plays VALUES (?, ?, ?)", events)
db.commit()
batch = [("e1", "s42", "2026-10-19"), ("e2", "s42", "2026-10-19"), ("e3", "s7", "2026-10-19")]
save_batch(batch)
save_batch(batch) # the phone retried after a dropped connection
for row in db.execute("SELECT song_id, COUNT(*) FROM plays GROUP BY song_id ORDER BY song_id"):
print(row)
# ('s42', 2)
# ('s7', 1)What just happened? The same three events arrived twice, but the table holds three rows, so song s42 shows 2 plays, not 4. A worker that simply added one per event would have doubled every count in the retried batch.
Mind how long you keep the ids. Offline plays often arrive days late, and a phone that stays offline past the 30-day window can send even older ones. Keep ids at least that long, and set a rule for events older than that.
How should the offline check handle a phone clock set backwards?
Store the latest time the app has seen inside the license, and refuse to play if the clock ever reads earlier than that. Otherwise a user can wind the date back and stretch a 30-day license forever.
from dataclasses import dataclass
from datetime import datetime, timedelta
MAX_OFFLINE = timedelta(days=30)
@dataclass
class License:
device_id: str
song_ids: set
last_check_in: datetime
last_seen: datetime
plan_active: bool
def can_play_offline(song_id, lic, device_id, now):
if lic.device_id != device_id:
return "wrong device"
if song_id not in lic.song_ids:
return "not downloaded"
if now < lic.last_seen:
return "clock went backwards"
lic.last_seen = now
if now - lic.last_check_in > MAX_OFFLINE:
return "go online"
if not lic.plan_active:
return "plan ended"
return "play"
start = datetime(2026, 10, 1)
lic = License("phone-1", {"s42"}, start, start, True)
print(can_play_offline("s42", lic, "phone-1", start + timedelta(days=10))) # play
print(can_play_offline("s42", lic, "phone-2", start + timedelta(days=10))) # wrong device
print(can_play_offline("s42", lic, "phone-1", start + timedelta(days=31))) # go online
print(can_play_offline("s42", lic, "phone-1", start + timedelta(days=5))) # clock went backwardsWhat just happened? Day 10 played. Day 31 asked the listener to go online. Then the phone's date was set back to day 5, and the check refused, because the license had already seen day 31.
The check only reads and updates the license, so it works with no network at all. In a real app, last_seen must be written back to secure storage after every play, or a restart would forget it.
It is not watertight. In our test, a phone set back to one minute after its last play still played, though ten real days had passed. Treat it as one layer, and keep the check-in window short.
How do you explain music streaming system design in a system design interview?
Explain it as three flows on one diagram: browsing the catalogue, playing a song and downloading for offline. Size the system first, then walk through each flow and name its main risk.
Ask the scope questions
Ask how many songs and users, which countries, whether offline is in scope, and whether podcasts or video are included. Write the answers on the board.
Do the quick estimate
Work out song size from bitrate and length, then total storage and peak bandwidth. Point out that bandwidth, not storage, drives the design.
Split metadata from audio
Draw the metadata database, cache and search index on one side. Draw object storage and the CDN on the other. Explain why audio never passes through the API servers.
Walk through one play
Show the rights check, the signed link, range requests for the first piece, adaptive bitrate and prefetching. Mention the encrypted local cache for repeat plays.
Walk through one download
Show encrypted files, a license per device, a device limit and a check-in window. State plainly what DRM can and cannot stop.
Finish with counting and scale
Add play events through a queue with unique ids. Then explain the festival spike: the CDN absorbs audio, and the queue absorbs events.
Questions people ask
- Does Spotify store a separate copy of each song for every quality?
- It did in 2010. Spotify's paper from that year describes Ogg Vorbis at quality q5, averaging roughly 160 kbps, and q9, averaging roughly 320 kbps, which premium users could choose in a setting. Today's formats may differ. Extra copies cost storage, but storage is small next to the bandwidth used to send music.
- Why not send audio through the app's own API servers?
- Because audio is far bigger than metadata, and a CDN is built to send big files from servers close to listeners. If the API servers sent every byte, they would need huge bandwidth and would slow down search and login. Signed links keep the API in charge of who may play, while the CDN does the sending.
- What is the difference between streaming and downloading a song?
- Streaming plays pieces as they arrive and keeps at most a temporary encrypted cache. Downloading saves the whole song on the phone for offline play. In a paid music app, both use encrypted files. The download also needs a saved license that stops working if the app does not check in, so a cancelled plan is noticed.
- Can I design a music streaming app with peer-to-peer sharing?
- You can, and Spotify's 2010 Windows and Mac clients did, alongside its servers. In its March 2010 measurement week, only 8.8% of the music data its clients used came from Spotify's servers. Peer-to-peer adds a lot of complexity, though, especially on phones with limited battery and data. For a new app, a CDN plus an encrypted local cache is the simpler choice.
- How does search work for songs in Indian languages?
- It works by storing several spellings of each title in the search index. People type the same song in native script or in English letters with many spellings. The index maps all of those spellings to the same song, allows small typos, and ranks the results by popularity so the likely song comes first.
- Why do music apps make you go online every few weeks?
- They make you go online so the app can renew your offline licenses and confirm your subscription is still active. Without a check-in, a cancelled user could keep playing downloads forever. As of 19 October 2026, Spotify asks users to go online at least once every 30 days to keep their downloads.
- When does a play count as a stream for royalties?
- Each service sets its own rule. As of 19 October 2026, Spotify for Artists says a stream counts when a listener plays a song for at least 30 seconds. Offline plays are counted when the listener goes online. A design should send each play as an event with a unique id, so retries never count twice.
Sources and further reading
- Spotify: Large Scale, Low Latency, P2P Music-on-Demand Streaming (Kreitz and Niemelä, Proceedings of IEEE P2P'10), sections II-B and II-C
- RFC 8216: HTTP Live Streaming (RFC Editor, Informational, August 2017), sections 2, 4.3.2.2, 4.3.2.4 and 6.2.4
- MDN: HTTP range requests (206, Content-Range, 416)
- Encrypted Media Extensions (W3C Working Draft, 13 October 2026)
- MDN: MediaKeys createSession() session types
- MDN: MediaKeySession load()
- Amazon CloudFront: Create a signed URL using a canned policy
- Spotify Support: Listen offline (download limits and 30-day rule)
- Spotify for Artists: How your streams are counted