Deciding When to Stop a Staged Rollout, Before You Have To — Agents Watch, I Halt
Field notes on building a Google Play staged-rollout watcher with Antigravity. Crash rate as a ratio to baseline, delayed ANR evaluation, and an explicit insufficient_data verdict — with the halt action kept in human hands.
One in the morning. I had just widened the rollout to 20%.
The crash rate sat slightly above the previous version — 0.44% against 0.31%. Not enough to stop. Not enough to sleep, either. I watched the dashboard for two hours.
By morning it had settled at 0.29%. Overnight traffic had come mostly from a cluster of older devices, and the sample had simply been too small.
Those two hours cost me more than sleep. They exposed something: I had never written down what would make me stop.
Deferring the halt criteria means deferring to fatigue
Working solo on Google Play apps, nobody else watches the rollout. At 1%, 5%, 20%, 50%, you look at numbers and choose: proceed, or pull it.
The trouble is that the choice tracks your own state more than the data. Late at night I lean optimistic. On the morning after a release, still tense, I lean the other way.
When people talk about handing rollout monitoring to an agent, the conversation usually slides toward handing it the halt button too. I drew the line elsewhere.
Let the agent collect and evaluate. Keep the halt action. In exchange, write the stopping conditions down first, in a form the agent can read.
Automate the criteria, not the decision. Follow that order and a two-hour vigil collapses into a ten-second glance.
The first obstacle is sample size, not thresholds
Early stages produce numbers you cannot trust. At 1% exposure my wallpaper apps see roughly 600–900 sessions a day. Three crashes in that window make the rate look alarming.
Read crash rate as a ratio, not an absolute
"Under 1% is healthy" ignores what your app actually does on a normal day. I anchor everything to the previous stable build still serving 100% of users.
If the old build sits at 0.28% and the new one at 0.42%, the ratio is 1.5. If the old build sits at 0.9% and the new one at 1.1%, the ratio is 1.22 — high in absolute terms, but no regression.
I set warn at 1.5x and halt_candidate at 2.0x. Nothing sacred about those numbers. They fall outside the 0.8–1.35 band my last twelve releases moved within.
ANR arrives late
Crashes surface on launch. ANRs surface after someone has actually used the app for a while. In a wallpaper app they appeared only after users had scrolled through dozens of images.
This turned out to be the single most useful piece of the design. Even when a threshold trips, if the window holds fewer than 800 sessions, no verdict is issued.
The agent returns insufficient_data and stays quiet until the next poll. Giving it a third answer beyond yes and no eliminated every false halt I had been suffering.
✦
Thank you for reading this far.
Continue Reading
What follows includes implementation code, benchmarks, and practical content we hope you'll find useful. This site runs without ads — server and development costs are supported entirely by members like you. If it's been helpful, we'd be truly grateful for your support.
WHAT YOU'LL LEARN
✦A three-state verdict: warn at 1.5x baseline crash rate, halt candidate at 2.0x
✦Why an explicit insufficient_data return below 800 sessions removed every false halt
✦A four-stage gate keyed to exposure volume rather than clock time, with numbers from four releases
Secure payment via Stripe · Cancel anytime
✦
Unlock This Article
Get full access to the rest of this article. Buy once, read anytime. This site is ad-free — your support goes directly toward keeping it running.
The loop queries the Play Developer Reporting API, normalizes the metrics, then passes them to the decision function. The agent holds a read-only service account credential — never a token that could publish or halt.
# rollout_watch.py — collect rollout metrics, hand them to a three-state verdictimport timefrom dataclasses import dataclassMIN_SESSIONS = 800 # below this, no verdictWARN_RATIO = 1.5 # relative to the previous stable buildHALT_RATIO = 2.0ANR_DELAY_HOURS = 12 # ANRs surface late@dataclassclass Snapshot: sessions: int crash_rate: float # 0.0042 == 0.42% anr_rate: float hours_since_start: floatdef fetch_snapshot(client, package: str, version_code: int) -> Snapshot: """Pull the last hour of metrics. Aggregation lags, so poll no faster than every 15 minutes.""" m = client.query_metrics( package=package, version_code=version_code, metrics=["crashRate", "anrRate", "distinctUsers"], window="PT1H", ) return Snapshot( sessions=int(m["distinctUsers"]), crash_rate=float(m["crashRate"]), anr_rate=float(m["anrRate"]), hours_since_start=m["elapsedHours"], )
The one-hour window is deliberate. Play's aggregation lag measured 40–70 minutes in practice. A five-minute window returned empty results, and the agent came close to reading emptiness as "zero crashes." Absent data and healthy data are not the same thing — obvious in prose, easy to lose in code.
For a long time I waited 24 hours per stage. That treats a Saturday and a Tuesday as equivalent, which they are not — my Saturday 5% produced about 1.4x the sessions of a Tuesday 5%.
So the waiting unit moved from hours to estimated sessions.
Stage
Exposure
Advance when
Median elapsed
1
1%
2,000 sessions reached, verdict continue
~38 hours
2
5%
6,000 sessions reached, verdict continue
~22 hours
3
20%
20,000 sessions reached, ANR evaluated
~18 hours
4
50%
Two consecutive continue verdicts
~9 hours
Stage 3 requires an ANR verdict because by then the sample problem has dissolved and ANR finally carries signal.
Stage 4 demands two consecutive verdicts so that a single lucky poll cannot walk the release to full exposure.
What four releases actually produced
Between April and July 2026 I ran four releases across two wallpaper apps through this loop.
halt_candidate fired once. On Android 15 devices only, switching themes drove the crash rate to 2.4x baseline. Six minutes passed between the notification and the rollout being stopped.
The old me would not have found that until morning.
insufficient_data came back 31 times. Every one of those windows was a moment I would previously have spent staring at a number and worrying. A verdict of "not yet" absorbs a remarkable amount of noise.
hold fired three times. Two resolved back to continue on their own. The third turned out to be a known ANR carried over from the previous build.
Verdict
Count
What followed
continue
142
Stage advanced
insufficient_data
31
Waited for the next poll
hold
3
Two self-resolved, one was a known ANR
halt_candidate
1
Halted manually after six minutes
What still bites
Choosing the baseline remains unsatisfying. The ratio assumes the previous build serves 100% of users, which breaks down when hotfixes stack up quickly.
For now, if the previous build has been live under 72 hours, I fall back to the build before it. That is a patch, not a design. Stratifying by OS version would be the honest answer.
The other trap: when the agent hit a Play Developer API rate limit, the exception was swallowed and the loop nearly returned continue. Exceptions now fall through to insufficient_data. Not to halt — to "no verdict." Get that wrong and a bad build sails to 100% unopposed.
The implementation was not the hard part. Putting words to "what makes me stop" was.
Fixing the numbers meant walking back through old releases and measuring how much they normally moved. Doing that, I learned what a normal day looks like for my own apps — for the first time.
Preparing to delegate the watch is what finally made my own operation visible to me. That inversion still sits with me.
Stratification by OS version comes next. Splitting the population before taking the ratio should make those late-night decisions shorter still.
If you are about to define halt criteria for your own rollout, I hope these notes give you somewhere to start.
Share
Thank You for Reading
Antigravity Lab is ad-free, supported entirely by members like you. We publish practical guides daily with implementation code, benchmarks, and production-ready patterns. If you've found it useful, we'd love to have you on board.