ANTIGRAVITY LABJP
Articles/App Development
App Development/2026-07-09Advanced

Background Refresh May Never Run — Measuring Execution Opportunity and Redesigning Around Freshness

Scheduling a BGTaskScheduler or WorkManager job is a request, not a guarantee. Here is how I instrumented execution opportunity, defined freshness as an SLO with p50 and p90 targets, and rebuilt the update path into three layers that hold up when background work never runs.

BGTaskSchedulerWorkManagerbackground processingAntigravity332app architecture2

Premium Article

A message arrived late one night from someone using my wallpaper app: "It's been showing the same image for three days."

On my own phone, the picture changed every morning without fail. I could not reproduce it. Only after pulling the logs did the situation come into focus. The background refresh task had been scheduled. It had simply never run. Not once.

Scheduling is a request. Whether the work executes is the operating system's decision. I knew that line by heart. On a real device in someone else's pocket, it carried an entirely different weight.

The assumption that quietly broke

My original design was naive. Schedule a BGAppRefreshTask overnight, prepare tomorrow's wallpaper before the user wakes up. On Android, do the same with a PeriodicWorkRequest on a 24-hour cadence. The widget just reads whatever image was prepared.

It works flawlessly in the simulator. It works on my iPhone, because I open the app daily and sleep with it on the charger.

My users are not me. Some keep Low Power Mode on permanently. Some open the app once a week. Some have added it to Android's battery optimization list. On those devices the task sits in the queue, its turn never comes, and the widget keeps displaying an image from several days ago.

Not a bug. Exactly the documented behavior. The design had simply misread reality.

Execution opportunity is a lottery, not an entitlement

iOS and Android use different vocabulary, but the shape is the same. The app declares what it would like to do. The OS inspects device state and grants a moment to do it. On some days no moment is granted.

Aspect iOS (BGTaskScheduler) Android (WorkManager)
Minimum interval earliestBeginDate is a floor, never a ceiling PeriodicWorkRequest floors at 15 minutes; execution depends on Doze windows
Execution window App Refresh typically hits expirationHandler around 30 seconds Workers get 10 minutes before isStopped is raised
Primary throttles Low Power Mode, app launch frequency, charging and network state Doze, App Standby Bucket, OEM-specific optimizations
After reboot Requests survive (re-register after an app update) Persisted, but a force stop halts everything until re-registration
Visibility of failure A run that never happened leaves no trace anywhere WorkInfo records state, but never the reason for deferral

That last row is the heart of the problem. Executions leave records. Non-executions leave nothing. So you never notice.

Android's App Standby Bucket demotes an app through active, working_set, frequent, rare, and restricted based on how it is used on that device. Once a weekly-use app lands in rare, its periodic window narrows to roughly once a day, if that. Mine was living there.

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
Minimal Swift and Kotlin instrumentation for logging execution opportunity, and what the resulting run rates actually looked like
Defining freshness as an SLO with p50 and p90 staleness targets, so background refresh becomes a contract rather than a hope
A three-layer update design that survives a job that never runs, plus where to draw the line when delegating measurement to an agent
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.

or
Unlock all articles with Membership →
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.

  • Copy-paste ready implementation code
  • New advanced guides published daily
  • $5/mo or $10 for lifetime access
View Membership →

Related Articles

App Dev2026-07-15
Quarantining the Dependencies Your Agent Adds, Before They Install
When an agent adds a dependency overnight, nobody reviews the lifecycle scripts that run at install time. Here is how I turned the default off and built a quarantine score to let the safe ones through.
App Dev2026-07-14
Protecting Screenshot Tests for AdMob Screens from Ad Nondeterminism
Screens with ads turn red on every screenshot run, and eventually nobody reviews the diffs. Here is a design that seals off AdMob banner nondeterminism and leaves only real layout breaks in your checks, with Compose code and Antigravity-driven diff triage.
App Dev2026-07-14
When the Deploy Was Green but Users Still Saw the Old Build: Field Notes on a Gate That Verifies Your Shipped Commit Reached the Edge
When a deploy reports success but production keeps serving the old build, a post-deploy gate that verifies your shipped commit actually reached the edge via a build stamp closes the gap. Field notes with real operational numbers.
📚RECOMMENDED BOOKS
Build a Large Language Model (From Scratch)
Sebastian Raschka
LLM Dev
Prompt Engineering for LLMs
Berryman & Ziegler
Prompting
AI Engineering
Chip Huyen
AI Eng
* Contains affiliate links
See all →