← Notes ·

The Cache That Outlived Its Assumptions

A caching bug that only existed because I copied a pattern without copying the reason it was safe.

I have a small API route that answers one question: what was a given day like, in one sentence each for what I was coding, what I was listening to, and what I wrote. It reads a log of Spotify plays off disk, figures out the day’s top artist, and returns a short phrase. Nothing exotic.

I built it by copying the shape of an older route that does almost the same thing for a personal dashboard — read the play log, cache it in memory so I’m not hitting the filesystem on every request, serve it back. Sensible, boring, correct. I shipped it, moved on, and a few days later got asked why today’s data wasn’t showing up even though the underlying log clearly had it.

The pattern was right. The context wasn’t.

The older route the caching came from only ever answers questions about days that have already fully happened — it’s a personal history browser, and history, by definition, isn’t still being written. Caching the play log forever is completely safe there: an already-elapsed day’s data doesn’t change after the fact, so there’s nothing for the cache to go stale against.

My new route gets asked about today constantly, and today’s play log is being appended to in real time by a background poller. The moment I copied the caching code, I also copied an assumption — “this data doesn’t change once read” — that was true in the route it came from and false in the route I was writing it into. Nothing in the code said so. It was true by context, not by anything visible on the page.

Assumptions don’t travel with a comment attached

This is the part that actually bothers me: the bug wasn’t a logic error. Every line did exactly what it looked like it did. Cache the array, return it, refresh never. The mistake was entirely in the gap between two routes’ unstated preconditions, and copy-pasting code copies the code perfectly while copying the reasoning behind it not at all, unless you go out of your way to write that reasoning down somewhere it’ll actually get read again.

I found it the boring way — comparing two endpoints reading the same underlying file, noticing one had today’s plays and the other didn’t, and working backward. Once I saw it, it was a two-line fix and an obvious one in hindsight. Obvious in hindsight is exactly the problem with this whole category of bug: it’s never obvious in foresight, because the code you’re copying from is, by definition, already proof that the pattern works. It worked. It just worked for reasons that didn’t apply to where I put it next.

bugsside-projectsinfrastructure