Skip to content
Plazoleta
· Raúl López

"Zero breakage" is almost never zero

LoverCast's profile migration didn't break anything visible, and it still wasn't free. On the difference between not breaking and not measuring what you break.

When we shipped the LoverCast profile migration, the phrase that came out naturally was "this doesn't break anything." And in the sense people usually mean by that phrase—nobody calls in furious because something of theirs got deleted—it was true.

But it wasn't zero. It was that we decided what would be lost, for how long, and for whom, before hitting the button. That's a different thing, and the difference matters more than it looks.

What actually happened

In Migrating the schema without breaking installed apps I walked through the mechanism: a compatibility view so old apps kept reading user_preferences as if nothing had changed. It worked for reads. For writes, unupdated Android/TV clients were stuck read-only until they installed the new version, and lost realtime sync across devices in the meantime.

That's written down, with dates and scope, in docs/MULTI_PROFILE_DEPLOY.md. Not as a footnote: as a section of the rollout plan, as legitimate as the step for applying the migrations.

The breakage exists. It's small, it's bounded to a subset of clients, and it disappears on its own once people update. But calling it "zero" would have been the project's first lie, and project lies have a habit of being believed first by whoever wrote them.

Why "zero breakage" is almost always the wrong phrase

The problem isn't the word "zero." It's what that word does in the decision-maker's head: if something is zero breakage, there's nothing to weigh, nothing to measure, nothing to decide. It's free. And schema migrations with clients you don't control are almost never free—the only thing you get to choose is where the cost lands and how long it lasts.

Expand-contract, which is the underlying technique in the LoverCast migration, doesn't eliminate that cost: it makes it negotiable. You add without breaking (expand), you live with both shapes for as long as needed, and only then do you clean up the old one (contract). That buys you time and options. It doesn't buy you freeness.

The compatibility view is the perfect example: on paper, "old clients won't even notice." In practice, they half-notice—they read fine, they write badly, and that's it. Discovering that before deploying, by testing writes and not just reads, is what separates a deliberate rollout from one you find out about through production logs.

The same pattern in the client you don't control

This isn't just a database lesson. It shows up anywhere there's a published client you can't force to update: a public API with third-party integrations, a file format you export, a mobile app with users on versions from eight months ago.

In all those places, the instinct is the same one we had at first with profiles: change the shape of the data and assume that if the old client doesn't explode, nothing happened. But "doesn't explode" isn't the same as "loses nothing." Sometimes it loses something quiet—realtime sync, a field that now arrives empty, a feature that degrades without warning—and that silence is worse than a visible error, because nobody investigates it.

What to do about it

It's not "never migrate without zero breakage," because that migration almost never exists when there are clients you don't update yourself. It's simpler and more uncomfortable:

  • Measure, don't assume. Test the old client's write path, not just the read path. LoverCast's breakage was exactly there, three layers below where we first looked (a detail of how PostgREST implements upsert).
  • Write it down. If you're embarrassed to put the breakage in the deployment document, that's a sign the decision wasn't as well thought out as you believed. Writing it down forces you to look it straight in the eye before anyone else sees it.
  • Bound it on purpose. "A subset of users loses a minor feature for two weeks" is not the same as "something breaks somewhere, for someone, and we don't know how much or when it gets fixed." The first is a decision. The second is a bet.

"Zero breakage" is almost never zero. It's "small, known breakage, deliberately accepted"—and that phrase, even though it sounds worse, is the one that actually holds up when someone asks what happened.


If you're in the middle of a migration with clients you don't control and want to compare notes, write to me.

Got a similar problem?

Tell us about it. You'll be talking straight to whoever writes the code.