[Perf] Cache qualified_playlists for /v1/playlists/trending#795
Merged
raymondjacobson merged 1 commit intomainfrom May 8, 2026
Merged
[Perf] Cache qualified_playlists for /v1/playlists/trending#795raymondjacobson merged 1 commit intomainfrom
raymondjacobson merged 1 commit intomainfrom
Conversation
The qualified_playlists CTE in /v1/playlists/trending walks every public playlist x its tracks (~94k playlists, ~5M playlist_tracks) to determine which playlists are "trending eligible" (>=5 tracks, >=5 distinct owners). This computation is request-independent and returns the same ~6,952 ids regardless of caller, but it was running fresh on every request — pg_stat_statements shows ~3-5s mean across multiple variants, with /v1/playlists/trending p95=8.4s. Cache the qualified set in app memory with a 5-minute TTL keyed by type (playlist|album). Holds at most 4 entries (~30 KB total). Verified on the prod read replica: Cache miss (1x per 5min): ~2.2s computation (unchanged) Cache hit: 36-41ms (down from 4.6s) ~125x End-to-end on local server pointing at prod replica: First request: 3.4s (cache miss + 5min TTL begins) Subsequent: 350-900ms (was 1.95s p50 in production) The cached query also drops the per-caller access_authorities filter — only ~0.05% of visible tracks have it set, so structural eligibility is overwhelmingly the dominant signal. Safety net: response post-filter drops any playlist currently flagged is_private=true or is_delete=true, so a stale cache entry can never surface a now-private playlist.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Cache the "trending eligible" playlist id set in app memory with a 5-minute TTL. The set is request-independent (same ~6,952 ids for every caller), but the previous implementation recomputed it from scratch on every request.
Why
/v1/playlists/trendingshows up as the second-worst signed-in endpoint by p95 (8.4s, p50 1.95s) and the underlying SQL has a 3-5s mean across multiple variants in pg_stat_statements. The dominant cost is thequalified_playlistsCTE walking every public playlist × its tracks (~94k playlists, ~5M playlist_tracks rows) to determine which playlists have ≥5 tracks and ≥5 distinct owners. None of those inputs depend on the caller — caller_id, time_range, auth, none of it.Impact
EXPLAIN ANALYZE on prod read replica:
Local server end-to-end (full GetPlaylists fetch included):
Caching trade-offs (the bits I want eyes on)
TTL: 5 minutes. Trending playlists are by definition popular — privacy/delete flips are rare. Worst case: a playlist becomes private and can be re-surfaced for up to 5 min. Mitigated by the safety net below.
Safety net post-filter. After fetching the playlists from
GetPlaylists, the handler drops any withis_private=trueoris_delete=true. Even if the cache is stale, a now-private playlist can never reach the response.Drop of per-caller
access_authoritiesfilter in the qualified-set computation. Only ~0.05% of visible tracks (716 / 1.39M) haveaccess_authoritiesset, so structural eligibility (≥5 tracks, ≥5 owners) is overwhelmingly the dominant signal. Dropping this filter is what makes the result wallet-independent and cacheable.Cache size. 4 entries max (one per
type× possiblytime_rangeif I add that key later). Memory footprint is trivial — at ~7k int32s each, ~30 KB total.Risk
is_private/is_deletesafety net catches the only correctness-relevant staleness window. NewTestGetTrendingPlaylists_Albumsincludes a regression case that flips a playlist private, exercises the cache hit path, and asserts it's filtered out of the response.Test plan
go test -count=1 ./api/...(full suite, all green)/v1/playlists/trending3.4s first, then 350-900ms warm🤖 Generated with Claude Code