Background Tasks for NativePHP Mobile#
Run scheduled artisan commands in the background on iOS and Android. Tasks continue to execute even after the user closes the app, with each task running on its own independent schedule.
Installation#
composer require nativephp/mobile-background-tasksphp artisan native:plugin:register nativephp/mobile-background-tasks
Usage#
Define your scheduled tasks in routes/console.php like you normally would:
use Illuminate\Support\Facades\Schedule; Schedule::command('sync:data')->everyFifteenMinutes();Schedule::command('notifications:check')->hourly();Schedule::command('backup:run')->daily()->whileCharging();
That's it. Each task is registered as an independent native background task with its own interval and constraints.
Constraints#
Attach device constraints to control when a task is allowed to run:
Schedule::command('sync:data') ->everyFifteenMinutes() ->onAnyNetwork(); Schedule::command('backup:run') ->daily() ->onWifi() ->whileCharging() ->whenBatteryNotLow();
Available Constraints#
| Method | Description |
|---|---|
onAnyNetwork() |
Requires any network connection (WiFi or cellular) |
onWifi() |
Requires WiFi (no metered/cellular) |
whileCharging() |
Device must be plugged in |
whenBatteryNotLow() |
Battery above ~15% |
whenStorageNotLow() |
Device storage not critically low |
whenIdle() |
Device in idle/doze state (screen off, not in use) |
Constraints are additive — all specified conditions must be met before the task runs.
Platform Constraint Support#
| Method | Android | iOS |
|---|---|---|
onAnyNetwork() |
WorkManager CONNECTED |
requiresNetworkConnectivity |
onWifi() |
WorkManager UNMETERED |
requiresNetworkConnectivity (no unmetered distinction) |
whileCharging() |
setRequiresCharging |
requiresExternalPower |
whenBatteryNotLow() |
setRequiresBatteryNotLow |
No equivalent (ignored) |
whenStorageNotLow() |
setRequiresStorageNotLow |
No equivalent (ignored) |
whenIdle() |
setRequiresDeviceIdle |
No equivalent (ignored) |
Long-Running Tasks#
By default, iOS uses BGAppRefreshTask for background execution. This has a 30 second execution limit shared across all tasks per launch — if iOS fires multiple tasks at once, they share that 30 seconds. Keep your artisan commands fast.
For tasks that need more execution time (large syncs, backups, ML processing), use longRunning():
Schedule::command('backup:run') ->daily() ->whileCharging() ->longRunning();
Long-running tasks use BGProcessingTask on iOS, which provides several minutes of execution time but only runs when the device is idle. iOS terminates processing tasks immediately if the user picks up the device. These tasks are typically delivered overnight.
When to use longRunning():
- Your artisan command takes more than ~25 seconds to complete
- You're doing heavy I/O, network transfers, or data processing
When NOT to use longRunning():
- Quick data syncs, notification checks, or lightweight polling
- Tasks that should fire reliably throughout the day
Note: Adding any constraint (
onAnyNetwork(),whileCharging(), etc.) automatically promotes the task toBGProcessingTaskon iOS, sinceBGAppRefreshTaskdoes not support constraints.
On Android, longRunning() has no effect — WorkManager handles all tasks uniformly regardless of duration.
Interval Limitations#
Android#
Android WorkManager enforces a minimum interval of 15 minutes. Any schedule frequency shorter than 15 minutes is automatically clamped to 15 minutes. Tasks fire reliably within the interval window.
iOS#
iOS controls all background execution timing. Your specified interval is a hint, not a guarantee. The OS decides when to run tasks based on:
- App usage patterns — iOS learns when you typically use the app and schedules tasks before predicted app launches. Freshly installed apps have no usage history and may see significant delays before tasks begin firing regularly.
- Battery state — Below ~20% battery or Low Power Mode enabled will prevent background tasks entirely.
- System daily budget — iOS allocates a finite daily budget for background execution across all apps.
- Background App Refresh — Must be enabled in Settings > General > Background App Refresh for the app.
- App Switcher visibility — If the user swipes the app away from the App Switcher, background tasks may stop.
- Recent usage — iOS will attempt to fulfill task requests within 2 days, but only if the user has used the app within the past week.
In practice, a task scheduled for every 15 minutes might fire every 15-30 minutes once iOS has learned the app's usage pattern, or it might take hours for freshly installed apps. This is a platform limitation, not a bug. Reliability improves over the first few days as iOS builds a usage profile.
Supported Schedule Frequencies#
| Laravel Method | Android Interval | iOS |
|---|---|---|
everyMinute() |
15 min (clamped) | OS discretion |
everyFiveMinutes() |
15 min (clamped) | OS discretion |
everyTenMinutes() |
15 min (clamped) | OS discretion |
everyFifteenMinutes() |
15 min | OS discretion |
everyTwentyMinutes() |
20 min | OS discretion |
everyThirtyMinutes() |
30 min | OS discretion |
hourly() |
60 min | OS discretion |
everyTwoHours() |
120 min | OS discretion |
everyThreeHours() |
180 min | OS discretion |
everyFourHours() |
240 min | OS discretion |
everySixHours() |
360 min | OS discretion |
daily() |
1440 min | OS discretion |
Unsupported Schedule Methods#
The following Laravel schedule methods are not supported and will be silently skipped:
twiceDaily(),twiceDaily(1, 13)— time-of-day scheduling is not supported on mobiledailyAt('13:00')— exact clock times cannot be guaranteedweeklyOn(),monthlyOn()— day-of-week/month scheduling is not availablecron('...')— custom cron expressions are not supported
Mobile background schedulers are interval-based, not clock-based. Only the interval methods listed above are supported.
Important Behavior#
Platform differences#
Android (WorkManager) is reliable and predictable — tasks fire close to their scheduled interval. iOS (BGTaskScheduler) is entirely at the OS's discretion — treat background tasks as "best effort" and design your app to work correctly even if tasks are delayed or skipped.
Each task runs independently#
Every artisan command is registered as its own native background task with its own interval, constraints, and execution lifecycle. Changing the interval or constraints for one task does not affect others.
Sync on every boot#
When the app launches, the Laravel scheduler is resolved and tasks are registered with the OS. Changed tasks are updated automatically. New tasks are added. Removed tasks will eventually expire from the OS scheduler as they are no longer re-registered.
Cold boot#
If the user kills the app, the OS will cold-start a new process to run the task. The plugin handles this transparently — a full PHP interpreter boots, runs the command, and shuts down. Cold boots take ~1-2 seconds.
Force-stop cancels everything#
If the user force-stops the app from system settings (Android) or swipes it away from the App Switcher (iOS), background tasks may stop. On Android, WorkManager re-registers tasks on next app launch. On iOS, swiping away from the App Switcher signals to the OS that the user doesn't want the app running, and background tasks may not fire until the app is opened again.
How It Works#
- App launch: The service provider resolves the Laravel scheduler and builds the task list
- Bridge call:
BackgroundTasks.Registersends the task list to the native layer - Register/Update: Each task is registered independently with the OS scheduler (WorkManager on Android, BGTaskScheduler on iOS). Existing tasks with the same name are updated.
- Background execution: When the OS fires a task, an ephemeral PHP interpreter boots, runs the artisan command, and shuts down
Build-Time Steps#
During the build, the plugin scans the Laravel scheduler and generates task identifiers for the iOS Info.plist (BGTaskSchedulerPermittedIdentifiers). Each task gets a unique identifier based on its command name (e.g., com.nativephp.task.sync-data).
Testing#
From PHP#
Use BackgroundTasks::runNow() to trigger all registered tasks immediately, bypassing intervals and constraints:
use NativePHP\BackgroundTasks\Facades\BackgroundTasks; BackgroundTasks::runNow();
From Xcode (iOS)#
You can force iOS to fire a background task immediately using Xcode's LLDB debugger. This bypasses iOS scheduling entirely and is the most reliable way to test.
- Run the app on a physical device from Xcode
- Wait for the app to fully launch and register tasks (look for
scheduled BGAppRefreshTaskin the console) - Background the app (go to the home screen)
- In Xcode, pause execution using the Debug > Pause button (or
⌃ + ⌘ + Y) - In the LLDB console at the bottom, type:
e -l objc -- (void)[[BGTaskScheduler sharedScheduler] _simulateLaunchForTaskWithIdentifier:@"com.nativephp.task.<your-command>"]
- Resume execution with Debug > Continue (or
⌃ + ⌘ + Yagain)
The task will fire immediately. The identifier is com.nativephp.task. followed by a slugified version of your artisan command name (colons and special characters become hyphens). For example, notifications:check becomes com.nativephp.task.notifications-check.
You can also simulate early termination of a task to test your expiration handling:
e -l objc -- (void)[[BGTaskScheduler sharedScheduler] _simulateExpirationForTaskWithIdentifier:@"com.nativephp.task.<your-command>"]
Note: These
_simulatemethods are private APIs available only in debug builds. They will not work in production or on the App Store.
From Android Studio#
Android WorkManager has a built-in test utility. No LLDB required — you can trigger tasks from the terminal while the app is running:
adb shell cmd jobscheduler run -f <your.app.package> <job-id>
Alternatively, use the App Inspection tab in Android Studio: navigate to Background Task Inspector to see all registered WorkManager tasks, their status, and trigger them manually.