RORK LABJP
TOOLING — Rork's developer repos keep moving: rork-xcode was updated on July 16, rork-device on July 15, and rork-plist on July 13OPUS46 — Claude Opus 4.6 is live in Rork, and Rork Max is built to assemble apps on top of Claude CodeSIM — A cloud iOS simulator runs in the browser, with one click to install on a device and two clicks to publish to the App StoreMAX — Rork Max emits pure Swift rather than React Native, reaching iPhone, iPad, Apple Watch, Apple TV, Vision Pro, and even iMessageNATIVE — That opens up HealthKit, ARKit and LiDAR, NFC, Dynamic Island, Live Activities, 3D through Metal, and on-device inference with Core MLSEED — Rork raised a $15M seed led by Left Lane Capital, with Peak XV and a16z Speedrun joining the roundTOOLING — Rork's developer repos keep moving: rork-xcode was updated on July 16, rork-device on July 15, and rork-plist on July 13OPUS46 — Claude Opus 4.6 is live in Rork, and Rork Max is built to assemble apps on top of Claude CodeSIM — A cloud iOS simulator runs in the browser, with one click to install on a device and two clicks to publish to the App StoreMAX — Rork Max emits pure Swift rather than React Native, reaching iPhone, iPad, Apple Watch, Apple TV, Vision Pro, and even iMessageNATIVE — That opens up HealthKit, ARKit and LiDAR, NFC, Dynamic Island, Live Activities, 3D through Metal, and on-device inference with Core MLSEED — Rork raised a $15M seed led by Left Lane Capital, with Peak XV and a16z Speedrun joining the round
Articles/Dev Tools
Dev Tools/2026-05-07Advanced

Keeping Rork Apps Fresh While Closed — iOS BGTaskScheduler, Silent Push, and Android WorkManager

A production-quality implementation guide for background refresh in Rork-built apps, covering iOS BGTaskScheduler, APNS Silent Push throttling, Android WorkManager, and the user-recovery funnel that ties it all together.

Rork515BGTaskScheduler3Silent PushWorkManagerBackground Refresh3React Native209Expo149

Premium Article

"The time when an app is closed is where most apps win or lose," an older developer told me years ago, and I have remembered it ever since. When I started building apps on my own, I only paid attention to the time users were actively in front of my screens. After a few years of running them, I came to feel — almost painfully — that what really mattered was what I could prepare during the time the app was closed. Whether the new feed appears within three seconds of launch, or only after five, will quietly move next-day retention by a point or two. I have watched it happen on my own dashboards more times than I can count.

When you try to add this kind of "stay-fresh-while-closed" behavior to an app generated by Rork, the differences between iOS and Android start to confuse you. Silent Pushes that you sent never seem to arrive. The BGTaskScheduler you registered does not fire. Android's WorkManager goes silent under Doze. You wrote it the way the docs said, and yet only half of what you intended is actually happening in production. Over the years of running apps that have crossed 50 million cumulative downloads, I have walked into this trap many times.

This article gathers what I learned, and walks through how to add production-grade background refresh to a Rork-built app — both the code patterns and the operational decisions that surround them.

Understand the iOS and Android background-execution models accurately

The first thing to do is to be very precise about what each OS will let you do in the background. If you are vague here, you will end up with an implementation that works on one platform and quietly does nothing on the other.

On iOS, background refresh has three main paths. BGAppRefreshTask is for short (around thirty-second) refreshes that the system schedules according to its model of when each user opens your app. BGProcessingTask is for longer work, lasting several minutes to a few hours, optionally constrained to charging or Wi-Fi. And Silent Push — APNS notifications with apns-push-type: background and content-available: 1 — is the server-driven trigger. These three are not mutually exclusive. In practice you combine them so that something gets through, even if any one channel is throttled.

On Android, the answer in 2026 is essentially WorkManager. The combination of OneTimeWorkRequest and PeriodicWorkRequest, the immediate execution path of ExpeditedWorkRequest, and the Constraints for network and charging conditions, are enough to produce a usable execution rate even under Doze and App Standby. The big advantage of WorkManager is that it absorbs OS-version differences for you — the same code runs on Android 8 through 15. As an indie developer, I cannot overstate how grateful I am for that.

The deciding difference between the two: iOS decides when to run on its own and your job is to maximize the chances that you get a slot, while Android lets the developer specify conditions and frequency with reasonable precision. With that frame in mind, we can move into the code.

iOS: a production-ready BGTaskScheduler setup

A Rork-generated Info.plist typically does not include any background-mode configuration by default, so first install expo-task-manager and expo-background-fetch, then declare background modes in app.json.

// app.json
{
  "expo": {
    "ios": {
      "infoPlist": {
        "UIBackgroundModes": ["fetch", "processing", "remote-notification"],
        "BGTaskSchedulerPermittedIdentifiers": [
          "net.rorklab.refresh",
          "net.rorklab.heavy-sync"
        ]
      }
    },
    "plugins": [
      ["expo-task-manager"],
      ["expo-background-fetch", { "ios": { "minimumInterval": 900 } }]
    ]
  }
}

A subtle thing to watch for: the identifier you register in BGTaskSchedulerPermittedIdentifiers must match exactly what you pass to BGTaskScheduler.shared.register in code. I once spent two days hunting a "the task isn't firing" bug that turned out to be a typo — net.rorklab.refresh versus net.rorklab.app.refresh. Define identifiers as constants in a shared file like constants/background.ts and import them on both sides.

While we are on the topic of identifier hygiene, plan ahead for the case where you want to retire an identifier. Once you ship an identifier in BGTaskSchedulerPermittedIdentifiers, removing it without a transition path means existing installs will silently fail to register the new identifier — they were granted permission for the old one. The cleanest pattern is to never delete identifiers, only deprecate them. Keep the deprecated identifier in the plist and unregister it explicitly in code on app launch. New installs and updated installs both end up in the right state.

A practical implication here: pick identifiers that are stable and descriptive of the work category, not the current implementation. Use net.rorklab.daily-sync, not net.rorklab.fetch-feed-v2. The latter looks tidy at first but you will regret it when v3 arrives and you need to coexist for a transition period.

In JavaScript, define a task with expo-task-manager and register it with BackgroundFetch.

// background-refresh.ts
import * as TaskManager from "expo-task-manager";
import * as BackgroundFetch from "expo-background-fetch";
import { fetchLatestFeed } from "./api/feed";
import { saveToCache } from "./store/cache";
 
export const REFRESH_TASK = "net.rorklab.refresh";
 
TaskManager.defineTask(REFRESH_TASK, async () => {
  const startedAt = Date.now();
  try {
    const feed = await fetchLatestFeed({ limit: 20, since: "auto" });
    await saveToCache("home_feed", feed);
    // Expected output: short completion log like "[refresh] 18 items in 1.2s"
    console.log(`[refresh] ${feed.length} items in ${(Date.now() - startedAt) / 1000}s`);
    return BackgroundFetch.BackgroundFetchResult.NewData;
  } catch (error) {
    console.warn("[refresh] failed", error);
    return BackgroundFetch.BackgroundFetchResult.Failed;
  }
});
 
export async function registerBackgroundRefresh() {
  const status = await BackgroundFetch.getStatusAsync();
  if (status === BackgroundFetch.BackgroundFetchStatus.Restricted ||
      status === BackgroundFetch.BackgroundFetchStatus.Denied) {
    return;
  }
  await BackgroundFetch.registerTaskAsync(REFRESH_TASK, {
    minimumInterval: 60 * 15, // iOS treats this only as a hint
    stopOnTerminate: false,
    startOnBoot: true,
  });
}

Two mistakes are common here. First, treating minimumInterval as the actual firing cadence. It is only a hint. iOS decides on its own based on device state and the user's habits — typically two to four times a day on real devices, with a tendency to cluster just before the user usually opens the app. Second, forgetting to return one of NewData, NoData, or Failed. If you forget, the system marks your app as a poor citizen and gradually starves it. I once shipped an app that quietly returned undefined and watched the firing rate decay day by day before I caught it.

There is one more iOS-specific behavior worth understanding before moving on. The system gives priority to apps the user actually opens. If your app is never opened for a week, BGAppRefreshTask will fire much less often regardless of what your code asks for. The implication is that background refresh is most effective for apps that already see daily engagement — it deepens what you have, but it cannot create engagement on its own. For an app that only gets opened once a week, server-driven Silent Push and the user-initiated AppState refresh become more important than BGTaskScheduler. I have learned to choose which mechanisms to invest in based on the engagement profile of the specific app, rather than implementing all of them by default. Apps with five-day-a-week users get all three layers; apps with two-day-a-week users get Silent Push plus AppState, and BGTaskScheduler is wired up but treated as a bonus.

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
You can move past the 'silent push doesn't arrive' wall by understanding APNS throttling and adopt a delivery design that actually reaches users in production
You'll get working code that bridges iOS BGTaskScheduler and Android WorkManager behind a single React Native interface — ready to drop into your Rork app
You can build a 'background-recovery funnel' that brings back roughly 30% of users who would otherwise drift away — using mechanisms that live entirely inside your app
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-05-28
Tracking Down BGTaskScheduler.submit Error Code=1 (Unavailable) in Rork iOS Apps
A field-tested checklist for diagnosing BGTaskScheduler.submit failing with Error Code=1 (Unavailable) in iOS apps built with Rork, walking through the six causes that account for nearly every case.
Dev Tools2026-07-15
The Comma That Fell to the Start of a Line: Rork, Quote Cards, and Japanese Line Breaking
React Native's Text component does not guarantee Japanese line-breaking rules. Here are the violation rates I measured, a WORD JOINER implementation that survives copy and VoiceOver, an onTextLayout audit harness, and how Rork Max (SwiftUI) differs.
Dev Tools2026-07-10
Adding React Compiler to Expo Let Me Delete 41 Hand-Written memo Calls
I enabled React Compiler on Rork-generated React Native screens and measured the rerender counts with Profiler. Here is how I decided which memo and useCallback calls were safe to delete, how to find the components the compiler bailed out on, and how to catch regressions in CI.
📚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 →