Every mobile app carries a local schema version alongside its data (SQLite's PRAGMA user_version, Room's @Database(version=), Core Data's model versioning). A migration only ever moves a device one step at a time — v→v+1→v+2 — because each step is a hand-written, testable transform; there is no direct v0→v5 jump.
n = target − device_version (steps owed, "migration debt")
P(step succeeds) = 1 − p
P(all n steps succeed unattended) = (1 − p)^n
That last line is why debt compounds: a device that stayed offline through three releases needs three consecutive successful steps just to catch up, and its odds of finishing unattended fall geometrically with n. A device that fails a step is quarantined at its last good version rather than left half-migrated — the safe behaviour real mobile ORMs use, since a partially-applied schema change can corrupt local data.
- Fleet size — how many devices are simulated (each an instanced sphere on the version staircase).
- Per-step failure — p above; higher values model riskier migrations (destructive column drops, large table rewrites on low-end storage).
- Online rate — the fraction of offline devices that reconnect and attempt their pending steps each tick.
- Release cadence — how often the target schema version bumps (an OTA app update), which is what generates debt in the first place: every device below the new target now owes another step.
- Ship a schema bump now — force an immediate release; Repair clears quarantined devices back to their last good version so they can retry.
Watch the "up to date" and "quarantined" readouts: raising failure probability or cadence while online rate stays low is exactly the field scenario that produces a growing tail of stuck installs an app team has to chase down with forced-reinstall prompts.