RORK LABJP
PRICE — Rork Max spans $200 to $1,800 per month, with the upper tiers aimed at heavier builders and teamsFREE — The free tier lands at roughly five prompts per week, enough to try it but not to build on continuouslySHIP — App Store publishing is automated through builds, certificates, and submission, so you can ship an iOS app without a Mac or XcodeSIM — A browser-streamed simulator lets you watch your app run in a real Apple environment from your own browserNATIVE — It reaches HealthKit, ARKit and LiDAR, NFC, Dynamic Island, and Metal 3D — territory React Native cannot touchFUNDING — Rork raised a $15M seed led by Left Lane Capital, announced April 9, 2026, and acquired app builder PaperlinePRICE — Rork Max spans $200 to $1,800 per month, with the upper tiers aimed at heavier builders and teamsFREE — The free tier lands at roughly five prompts per week, enough to try it but not to build on continuouslySHIP — App Store publishing is automated through builds, certificates, and submission, so you can ship an iOS app without a Mac or XcodeSIM — A browser-streamed simulator lets you watch your app run in a real Apple environment from your own browserNATIVE — It reaches HealthKit, ARKit and LiDAR, NFC, Dynamic Island, and Metal 3D — territory React Native cannot touchFUNDING — Rork raised a $15M seed led by Left Lane Capital, announced April 9, 2026, and acquired app builder Paperline
Articles/Dev Tools
Dev Tools/2026-05-07Advanced

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

Silent Pushes that never arrive. A BGTaskScheduler that stays quiet. This is how I pushed background refresh in a Rork-built app up to production quality on both iOS and Android — and what the success-rate numbers told me to fix first.

Rork532BGTaskScheduler3Silent PushWorkManagerBackground Refresh3React Native224Expo165

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. I have fallen into this trap more than once building on my own — including a stretch of several days lost to what turned out to be a single registration call sitting one line too late.

What follows is the record of closing those traps one by one: the code that ended up working, and the operational calls I made after looking at the success-rate numbers.

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
When BGTaskScheduler.submit returns Error Code=1, the cause space is finite — six of them. Includes the identifier trap specific to Expo-based Rork apps and a getPendingTaskRequests self-check, ordered the way I actually hit them.
Dev Tools2026-07-30
What Renovate may bump in an Expo app, and what it must never touch
Turning on automated dependency updates in a Rork-generated app also hands Renovate the 123 packages Expo SDK 57 pins. Measured on 2026-07-30, six of them sit a full major version behind npm latest. Here is how to generate the ignore list from the SDK instead of maintaining it by hand.
Dev Tools2026-07-28
Counting what prebuild --clean will erase before you upgrade to Expo SDK 57
A raw diff between two generated ios/ trees showed 649 changed lines; only 3 were real edits. How to count what prebuild --clean erases, and move it into a config plugin.
📚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 →