Skip to main content
2026-09-21SubsonicPerformance BaselinePureStream

500,000 Songs, 69 Microseconds: Benchmarking PureStream's Subsonic Server Read Path

We pushed the library to 500,000 songs and benchmarked every read path of the Subsonic server: main enumeration in 69 microseconds, a 250k-deep offset page in 25 ms, and songs-by-genre in 113 microseconds. Two super- linear hotspots were found and logged.

500,000 Songs, 69 Microseconds: Benchmarking PureStream's Subsonic Server Read Path

Many self-hosted streaming setups quietly slow down as the library grows: the first pages feel snappy, then deep pages start to spin. We pushed PureStream’s Subsonic server to 500,000 songs and measured every read path—no thresholds, no pass/fail, just real numbers.

🛠 Why 500k

A serious music library is not “a few thousand tracks”; for collectors it is hundreds of thousands. The most-used Subsonic endpoints—search3 enumeration, getMusicDirectory browsing, the getAlbumList2 album grid—have very different cost shapes at scale: some ride covering indexes, others must deduplicate across the whole table. Until it is measured, “fast on large libraries” is just a slogan.

Three things happened in this round:

  • [x] Seeded a 500k-row catalog (~42k albums, 2,000 artists, 100 genres) through the full migration chain—a production-shaped SQLite catalog
  • [x] Added a missing read index for genre queries and registered it in the schema contract—if the index is missing or dropped, startup fails loudly
  • [x] Sampled P50/P95 across 14 read paths and wrote the numbers to a baseline report

📊 Measured results

Endpoint 100k 500k Verdict
search3 main enumeration 53µs 69µs ✅ Flat
search3 deep page (offset=250k) — 25ms ✅
getMusicDirectory single page 1.3ms 1.4ms ✅
getAlbumList2 three paging modes 8–82ms 86–333µs ✅
getSongsByGenre untested 113µs ✅ New index pays off
FTS text search 24.8ms 137ms ⚠️ Linear
getArtists dedup enumeration 3.4ms 543ms ❌ Super-linear hotspot
getGenres aggregation untested 600ms ⚠️ Acceptable ceiling
Huge single-directory deep page — 734ms ⚠️ Edge shape

Numbers worth calling out:

  • 69µs main enumeration: the empty-query search3 first page stays microsecond-fast at 500k rows, thanks to a partial covering index that only indexes non-missing rows—so catalog churn from the scanner never perturbs the read path.
  • 25ms deep offset: fetching a page at position 250,000 costs 25 ms. Deep paging is not free, but it stays far from seconds.
  • 113µs songs-by-genre: the direct payoff of this round’s new index—before the change this path was a full table scan; EXPLAIN now confirms an index search.

⚠️ Two hotspots on record

A baseline’s job is also to quantify the bad news:

  • 543ms for getArtists: artist deduplication unions two role columns then groups—every page scans the whole table. At 500k it still lands under a second, so we logged it as acceptable rather than scheduling a fix.
  • 600ms for getGenres: genre aggregation walks the index in order, but COUNT(DISTINCT album) still runs per group—600ms is the honest cost at 500k rows. Acceptable, with a ceiling.

✅ What shipped

  • [x] A full 500k read-path baseline: P50/P95 for 14 endpoints
  • [x] Genre read index + schema-contract registration + EXPLAIN assertions (regression-proof)
  • [x] Quantified records of two super-linear hotspots, with fix directions

⏳ What’s next

  • [ ] Cursor-based full enumeration for endpoints without an offset parameter, removing repeated deep scans
  • [ ] Folding the three identity reads into one for in-flight streaming checks

500,000 songs, 69 microseconds—“fast on large libraries” is not a slogan, it’s an EXPLAIN plan and a P50 sample.