ANTIGRAVITY LABJP
Articles/App Development
App Development/2026-06-23Advanced

My Daily Wallpaper Widget Stopped Updating — Measuring WidgetKit's Reload Budget and Rebuilding the Design

A widget that was supposed to rotate the wallpaper every day froze after a few days. Here is how I measured WidgetKit's timeline reload budget and extension memory limit, then rebuilt the design around a single daily timeline.

WidgetKit3iOS27Antigravity338wallpaper appperformance12

Premium Article

I added a feature to my wallpaper app that rotates the home-screen widget image every day. In the simulator it behaved beautifully. On a real device left running for a few days, the widget froze one morning — the same image, hour after hour, never changing. Opening the app fixed it. Closing it and waiting brought the freeze back.

My first guess was a caching bug or a bad date calculation. But the cause was not a coding mistake. As an indie developer running this widget for real, I was hitting WidgetKit's "reload budget" — a hard limit that appears nowhere on screen — every single day. It is a place you reach the moment you take widgets seriously, so I want to leave behind the numbers I measured and the way I rebuilt the design.

Widgets Are Not "Alive"

There is one misconception worth clearing up first. A widget is not a small app running continuously. What you see is a still frame the OS draws from a prepared "timeline."

A timeline is an array of TimelineEntry values, each one a declaration: "at this time, show this content." The system calls getTimeline(in:completion:), receives the array, and swaps the view when each entry's date arrives. Your code does not run on every swap. The moment you hand back the array, everything downstream belongs to the OS.

struct WallpaperProvider: TimelineProvider {
    func placeholder(in context: Context) -> WallpaperEntry {
        WallpaperEntry(date: Date(), assetID: "placeholder")
    }
 
    func getSnapshot(in context: Context, completion: @escaping (WallpaperEntry) -> Void) {
        completion(WallpaperEntry(date: Date(), assetID: currentAssetID()))
    }
 
    func getTimeline(in context: Context, completion: @escaping (Timeline<WallpaperEntry>) -> Void) {
        // Build the whole array here. The number of calls is what spends the budget.
        let entries = buildDailyEntries(from: Date())
        completion(Timeline(entries: entries, policy: .atEnd))
    }
}

So if you translate "change the wallpaper daily" into "call getTimeline daily to fetch a new image," you have already taken a wrong turn. That is exactly the turn I took first.

The Invisible Limit Called Reload Budget

There are three main triggers that call getTimeline again: the timeline's reloadPolicy expiring, the app calling WidgetCenter.shared.reloadTimelines(ofKind:), and the system deciding on its own.

Background reloads among these draw from a per-widget daily budget. Apple does not publish the exact figure, but for frequently visible widgets a range of roughly 40–70 reloads per day shows up repeatedly, both in the developer community and in my own measurements. The budget replenishes gradually over the day.

My first implementation called reloadTimelines on every foreground and on a one-hour timer. The timeline itself was a short "one entry now plus .atEnd an hour later." The result was around 120 background reloads requested per day. I burned through the budget by late morning, and the OS quietly deferred reloads for the rest of the day. That was the real cause of "freezes one morning."

There is no API that tells you whether the budget is exhausted. No error fires. The image simply goes stale. The nastiest part of this trap is that the budget is generous on the simulator and on a device during active development, so it only surfaces after the app has been left alone for a while.

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
Switching from per-request reloads to a 24-hour timeline cut background reloads from roughly 120/day to 3/day in my own measurements
Fixing the ~30MB widget extension memory ceiling that killed full-resolution wallpapers, dropping decoded memory from ~38MB to 0.4MB with ImageIO
Instrumentation that indirectly surfaces the OS reload budget by counting getTimeline calls in an App Group container
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-04
Paid, but the Ads Won't Go Away — Closing a StoreKit 2 Transaction.updates Launch Race with Antigravity
How I traced a StoreKit 2 launch race that dropped transactions arriving right after startup, pinned the listener to the app's lifetime instead of a view's, reconciled with currentEntitlements, and let Antigravity turn it into a StoreKitTest regression suite.
App Dev2026-07-03
When Universal Links Break Silently — Catching Association Drift with an Agent-Run Verification Gate
Universal Links and App Links fall back to the browser with no error when your association files or entitlements drift apart. Here is a design that generates the files from one source of truth and hands weekly and pre-release checks to an agent.
App Dev2026-06-23
When StoreKit 2 Users Say They Paid but Can't Access — Field Notes on Subscription Entitlement Drift
StoreKit 2 subscriptions are harder to operate than to implement. This is a record of the drift between currentEntitlements, subscription.status, and server notifications — and the reconcile logic that finally stopped the support tickets.
📚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 →