◉RORK LABJP
●GPT-6.1 SOL — Added to Rork's model menu (09-29) with Low, Medium and High effort; included on Pro and Max●SONNET 5.5 — Claude Sonnet 5.5 is selectable: strong at UI design and faster than Sonnet 5 (09-28)●SONNET 4.5 — Retires on the Claude API on 11/30, 27 days left. If your app calls it, check the migration target●SDK 58 — Moves to React Native 0.88. R8 is now on by default in Android release builds, and iOS prebuild generates a SceneDelegate●OBSERVE — EAS Observe now records native crashes too (SDK 57.0.21 or later, 10/07)●NEW — My App Open Ad Only Showed When the App…●GPT-6.1 SOL — Added to Rork's model menu (09-29) with Low, Medium and High effort; included on Pro and Max●SONNET 5.5 — Claude Sonnet 5.5 is selectable: strong at UI design and faster than Sonnet 5 (09-28)●SONNET 4.5 — Retires on the Claude API on 11/30, 27 days left. If your app calls it, check the migration target●SDK 58 — Moves to React Native 0.88. R8 is now on by default in Android release builds, and iOS prebuild generates a SceneDelegate●OBSERVE — EAS Observe now records native crashes too (SDK 57.0.21 or later, 10/07)●NEW — My App Open Ad Only Showed When the App…
Articles/Dev Tools
⬡ Dev Tools/2026-06-14Advanced

Actually Delivering 'It Updates Without Opening' in Expo — A Realistic Background Task Design

Building 'content refreshes every morning' into a Rork-generated Expo app runs into iOS background execution being far less dutiful than you expect. Here is a minimal expo-background-task setup plus a design that doesn't break when the task never runs.

Rork611Expo234Background TaskBGTaskScheduler4iOS118indie developer52

✦ Premium Article

Running a wallpaper app as an indie developer, the single most requested feature was "I want a new wallpaper every morning without opening the app." Naively, you would just run something periodically in the background and swap the image. But build this seriously on iOS and you end up chasing irreproducible bug reports: "it updated yesterday but not today."

The cause is not a bug in your code — iOS background execution is designed around the premise that you cannot know when it will run. Rork generates an Expo app, but underneath it iOS BGTaskScheduler is at work, and if you build without understanding its capriciousness, your UX is left to chance. This article covers building a refresh experience that survives the task not running, on both the wiring and the judgment side.

Why "It Should Update Every Morning" Doesn't

iOS background tasks are not something you can pin to a clock and force to run. The system runs them only when it decides, based on the device's usage patterns, battery, and network, that "now is acceptable."

So they run fairly often for users who open the app daily, and rarely for users who almost never open it. Ironically, the dormant users who most want "updates without opening" are exactly the ones whose background tasks rarely fire. Accept this asymmetry first or you will design wrong.

A Minimal expo-background-task Setup

Current Expo uses expo-background-task (which uses BGTaskScheduler / WorkManager internally), not the deprecated expo-background-fetch. The task itself is defined with expo-task-manager.

import * as TaskManager from "expo-task-manager";
import * as BackgroundTask from "expo-background-task";
 
const REFRESH_TASK = "daily-wallpaper-refresh";
 
TaskManager.defineTask(REFRESH_TASK, async () => {
  try {
    const updated = await fetchAndCacheTodaysWallpaper();
    // Always return success/failure. Not returning makes iOS shrink your future budget
    return updated
      ? BackgroundTask.BackgroundTaskResult.Success
      : BackgroundTask.BackgroundTaskResult.Failed;
  } catch {
    return BackgroundTask.BackgroundTaskResult.Failed;
  }
});
 
export async function registerRefreshTask() {
  const status = await BackgroundTask.getStatusAsync();
  if (status !== BackgroundTask.BackgroundTaskStatus.Available) return;
  await BackgroundTask.registerTaskAsync(REFRESH_TASK, {
    minimumInterval: 60 * 12, // minutes; 12 hours here
  });
}

In app.json, iOS UIBackgroundModes must include processing. In Expo you write it under infoPlist.

{
  "expo": {
    "ios": {
      "infoPlist": {
        "UIBackgroundModes": ["fetch", "processing"],
        "BGTaskSchedulerPermittedIdentifiers": ["daily-wallpaper-refresh"]
      }
    }
  }
}

Forget to list the task ID in BGTaskSchedulerPermittedIdentifiers and registration succeeds but the task never runs on device — a silent failure. I lost half a day to exactly this.

✦

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
✦Get a working minimal setup of expo-background-task and expo-task-manager, including the UIBackgroundModes configuration
✦Understand that minimumInterval is a floor, not a guarantee, expressed through concrete iOS conditions: Low Power Mode, launch frequency, charging state
✦Implement a two-tier foreground fallback that keeps a wallpaper app's auto-refresh from breaking when background never fires
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

Rork 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

⬡ Dev Tools2026-09-05
Your Widget Extension Can Submit a BGTaskScheduler Request. It Just Cannot Register the Handler
Calling BGTaskScheduler from a widget extension compiles, submits, and returns true — and then nothing runs. Here is why registration belongs to the host app only, and how I moved refresh ownership back where it belongs across my wallpaper apps.
⬡ Dev Tools2026-05-28
Tracking Down BGTaskScheduler.submit Error Code=1 (Unavailable) in Rork iOS Apps
When BGTaskScheduler.submit returns Error Code=1, the cause space is finite — six of them. Covers telling Code=1 from Code=3, the identifier trap in Expo-based Rork apps, submitting from a widget extension by mistake, and a getPendingTaskRequests self-check.
⬡ Dev Tools2026-10-30
It runs in development but the iOS Release build dies on launch — checking whether a missing scheme is the cause
In an expo-router app, an app.json with no scheme can crash only the iOS Release build at launch. Why Debug and Android survive, the two ways to fix it, how to think about leaving the scheme out on purpose, and a pre-submission check.