For most of this year I've been building two apps at once: Resleeve, which turns albums into wallpapers, and Score, a music-ranking app. Different products, but both need the same raw material - data about albums, artists and songs. Both were calling MusicBrainz directly, both were re-fetching the same records over and over, and both were quietly competing for the same rate limit.
MusicBrainz allows one request per second, per IP address - and my whole stack sits behind a single IP. Two apps hammering it independently is a good way to get throttled by your own software. So I stopped duplicating the music layer and pulled it out into a service of its own: Prelude. One catalogue that every app talks to instead of MusicBrainz - it caches everything it fetches, and it's the only thing in my stack allowed to talk to the outside world.
The cache is just a table
The core of Prelude is deliberately unglamorous: a single Postgres table, music_item, keyed by the MusicBrainz ID. Every entity - an album, an artist, a song - is one row: its name, its artist, a cover-art URL, and the full raw response stored as jsonb in case I need a field I didn't extract.
The whole service is a read-through cache. A request comes in for an album; Prelude checks the table; if the row is there it comes back in a millisecond and nothing touches the internet. If it isn't, Prelude fetches it, saves the row, and returns it. The first person to ever ask for an album pays the cost; everyone after gets it instantly. The catalogue teaches itself - every miss becomes a future hit, and over time the table fills with exactly the music people actually care about.
Two ways to ask
There are only two things you can do with Prelude, and keeping them apart mattered. Get is retrieval - you have an exact ID and you want that one record. Search is discovery - you have the words "ok computer" and you want a list of candidates to pick from. Get is cache-first: check the table, fetch on a miss. Search is authoritative: it always asks MusicBrainz, because my cache only holds a slice of the catalogue and can't be trusted to return a complete list of matches.
The fiddly part was deciding what each type points at. MusicBrainz models music more precisely than people think about it - an "album" is a release-group rather than a specific pressing, a "song" is a recording rather than the abstract composition. Picking the level that matches how someone actually means the word, and working out which entity carries the artist name and which one has cover art, took more back-and-forth with the data model than I expected.
The hard part: one request per second
The interesting engineering in Prelude isn't the cache - it's what happens on a miss. A miss means going out to MusicBrainz, and that one-request-per-second limit is shared across everything I run. If every web request called out the moment it missed the cache, a burst of traffic - or both apps at once - would blow straight through the limit and get the whole stack throttled.
So nothing in Prelude calls MusicBrainz directly. Every outbound request is handed to a single background worker - a Celery task behind a Redis queue - and that worker is rate-limited to one request per second. One worker, one queue, one polite connection to the outside world. The cache makes the trip out rare; the queue makes it safe. A web request that misses doesn't fetch anything itself - it drops a job on the queue and waits for the result to show up in the cache.
Behaving under load
A queue on its own isn't enough; the worker still has to spend its one-per-second budget wisely. Three details do most of that work:
- De-duplication. If fifty people request the same brand-new album at the same instant, I don't want fifty identical fetches. Before queuing, a request tries to claim the ID with a single atomic Redis operation (
SET … NX EX); only the first one wins and creates a job, and the rest just wait for the cache to fill. Fifty requests collapse into one fetch. - A negative cache. Ask for an ID that doesn't exist and the worker leaves a short-lived tombstone in Redis, so the request gets a clean 404 in a second instead of hanging while it waits for a row that's never coming.
- An API that never blocks. The endpoint that waits on a fetch is fully asynchronous - it queues the job, then polls the cache on the event loop, returning the instant the row appears or giving up after a timeout. Dozens of requests can sit parked on a slow fetch without tying up a single thread.
Running it for real
Prelude lives on a small Proxmox container, reaching its Postgres and Redis over a private Tailscale network and exposed to the world through a Cloudflare tunnel. The one twist compared to a normal web app is that it's two processes, not one - the FastAPI server and the Celery worker - each running as its own systemd service so they come back on crash and on boot. A third service runs Flower, Celery's dashboard, so I can watch the queue live: every request coming in, what the worker's fetching, how long it took. That one's kept private over Tailscale - it's an operator's window, not something the public should see.
Public, but not open
Prelude is reachable on the internet, but it does real work on every miss, so I didn't want it open to anyone. I put Cloudflare Access in front of it: my apps authenticate machine-to-machine with service tokens - their backends send a token header on every call - I get in as a human with an email login, and the only thing left open to the world is the cover-art images, because a browser loading an <img> can't send an auth header. A public URL that only answers to my apps and me.
The invisible piece
Prelude is the one part of the stack nobody will ever see. No UI, no landing page, nothing to visit - Resleeve calls it, Score will call it, and users just notice that albums load instantly and the artwork is always there.
It's the only piece of the stack a user never sees - and the one that makes everything else fast.
It's also the most infrastructure-heavy thing I've built: a self-filling cache, a rate-limited job queue, de-duplication, a negative cache, a multi-service deployment behind a private network. None of it shows up on screen, which is exactly the point. The best infrastructure is the kind you forget is running.
For the finished project overview, read the Prelude case study.