๐ฏ The Problem
I needed to build a CRM mobile app that syncs call logs continuously in the background - even when:
- โ The app is completely closed
- โ The phone is locked
- โ Hours have passed with no user interaction
- โ The user swipes away the notification
This is NOT a simple background task. This is a 24/7 service that needs to be bulletproof.
๐ง The Failed Attempts
Attempt 1: AppState + Background Timers โ
// This gets killed within seconds
AppState.addEventListener('change', (state) => {
if (state === 'background') {
setInterval(() => syncData(), 5000); // Android kills this instantly
}
});
Result: Dead on arrival. Android kills it immediately.
Attempt 2: react-native-background-fetch โ ๏ธ
BackgroundFetch.configure({
minimumFetchInterval: 15, // Can't go lower than 15 minutes!
}, async (taskId) => {
await syncData();
BackgroundFetch.finish(taskId);
});
Problem:
- Minimum interval is 15 minutes
- Android decides WHEN to run it (unreliable)
- Not suitable for real-time needs
โ The Solution: react-native-background-actions
This library creates a TRUE Android foreground service using HeadlessJS.
Installation
npm install react-native-background-actions
The Key Implementation
Here's the critical code that makes it work:
import BackgroundService from 'react-native-background-actions';
class BackgroundSyncService {
static async init() {
const options = {
taskName: 'DataSync',
taskTitle: 'Sync Active',
taskDesc: 'App is syncing data',
taskIcon: {
name: 'ic_launcher',
type: 'mipmap',
},
};
// ๐ฏ THIS is the magic - starts a true foreground service
await BackgroundService.start(this.backgroundTask, options);
}
// ๐ฅ The infinite loop that keeps your service alive
static backgroundTask = async () => {
while (BackgroundService.isRunning()) {
try {
// Your sync logic here
await this.performSync();
// Check every 2 minutes (120000ms)
await this.sleep(120000);
} catch (error) {
console.error('Sync failed:', error);
await this.sleep(120000);
}
}
};
static sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
static async performSync() {
// Get data from local storage/database
const newData = await this.getNewData();
// ๐ฏ KEY: Only hit API if there's actually new data
if (newData.length > 0) {
await this.sendToBackend(newData);
}
}
static async stop() {
await BackgroundService.stop();
}
}
๐คฏ The WTF Moment: Swipeable Notification?
After implementing this, I noticed something weird:
The notification was swipeable!
I thought: "Wait, this can't be a real foreground service then..."
So I tested it. Swiped away the notification. Closed the app. Locked my phone.
4 HOURS LATER - I checked my backend database.
IT WAS STILL SYNCING! ๐คฏ
๐ The Investigation: Is This Really a Foreground Service?
I dove deep into the library's source code and Android documentation. Here's what I discovered:
YES, It's a Real Foreground Service!
The library uses Android's official startForeground() API:
// Inside the library's native code
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
Notification notification = createNotification();
// ๐ฏ THIS makes it a foreground service
startForeground(SERVICE_ID, notification);
startTask();
return START_STICKY;
}
The notification builder also sets .setOngoing(true) correctly:
NotificationCompat.Builder builder = new NotificationCompat.Builder(context, CHANNEL_ID)
.setOngoing(true) // Should make it non-dismissible
.build();
So Why Can I Swipe It Away?
Here's the plot twist: Android changed the rules!
Before Android 14:
-
setOngoing(true)= Non-dismissible notification - Some manufacturers (Xiaomi, Honor, OnePlus) already allowed dismissing on Android 9+
Android 14+:
- Google officially changed the behavior
- ALL foreground service notifications can now be dismissed
- Exceptions: CallStyle notifications (actual phone calls) and media playback
Critical Insight:
Dismissing the notification โ Stopping the service
The service keeps running even after you swipe away the notification!
โก Performance: The Battery Trap
My Initial Mistake
await this.sleep(5000); // โ Every 5 seconds - BATTERY KILLER
Problem:
- Wakes device 720 times per hour
- Drains battery significantly
- Users will uninstall your app
The Smart Solution
await this.sleep(120000); // โ
Every 2 minutes - Reasonable
Why this works:
- Only checks LOCAL data (no network calls)
- API call happens ONLY when there's new data
- 24x fewer wake-ups
- Much better battery life
Key Pattern:
const newData = await getLocalData();
// ๐ฏ Only hit the network if there's actually something new
if (newData.length > 0) {
await sendToAPI(newData);
}
๐ Required Setup
AndroidManifest.xml
<manifest>
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.WAKE_LOCK" />
</manifest>
Usage in Your App
// Start on login
await BackgroundSyncService.init();
// Stop on logout
await BackgroundSyncService.stop();
๐งช Real-World Test Results
Platform: Android 9
Duration: 4+ hours continuous operation
Results:
- โ Survived app being closed
- โ Survived notification being swiped away
- โ Survived device lock/unlock cycles
- โ All data synced correctly
- โ No crashes or memory leaks
- โ Acceptable battery consumption
๐ Key Learnings
1. Foreground Services Are NOT What You Think
Modern Android (9+, especially 14+) allows users to dismiss foreground service notifications. This is intentional by Google for better UX.
2. HeadlessJS Is Powerful
React Native's HeadlessJS allows JavaScript to run independently of the UI. Combined with a foreground service, it's incredibly reliable.
3. Optimize Check Intervals
Don't check every few seconds. Use 1-2 minute intervals and only hit your API when there's actual new data.
4. Local First, Network Second
Always check local data first. Only make network calls when necessary.
๐จ Common Pitfalls to Avoid
โ DON'T: Poll Every Few Seconds
await this.sleep(5000); // Battery drain!
โ DO: Use Reasonable Intervals
await this.sleep(120000); // 2 minutes is fine for most use cases
โ DON'T: Hit Your API on Every Check
// Bad: Always calling API
await sendToAPI(getAllData());
โ DO: Only Send New Data
// Good: Check locally first
const newData = await getNewData();
if (newData.length > 0) {
await sendToAPI(newData);
}
๐ฏ When to Use This Approach
Good for:
- โ CRM apps that need to track user activity
- โ Field service apps
- โ Location tracking apps
- โ Real-time data sync requirements
- โ Apps that need to work offline
Not good for:
- โ Simple occasional background tasks (use WorkManager)
- โ Infrequent syncs (use background-fetch)
- โ Push notification-based updates
๐ฎ Alternatives to Consider
For Periodic Tasks (15+ min intervals)
Use react-native-background-fetch - it's more battery-efficient for infrequent tasks.
For True Non-Dismissible Notifications
Use @notifee/react-native with CallStyle notifications (only for actual phone call apps).
For Native Control
Write a native Android foreground service if you need full customization.
๐ก Final Thoughts
Building a reliable background service in React Native is tricky, but react-native-background-actions makes it possible. The key insights:
- It IS a real foreground service - despite the swipeable notification
- Modern Android allows dismissing notifications - this is by design
- Optimize your check intervals - balance responsiveness with battery life
- Local checks, conditional API calls - don't spam your backend
The notification being dismissible threw me off at first, but after understanding Android's evolution, it all makes sense.
The service works. It's reliable. And it survives everything.
๐ Resources
- react-native-background-actions GitHub
- Android Foreground Services Documentation
- React Native HeadlessJS
Have you built background services in React Native? What challenges did you face? Drop a comment below! ๐
If this helped you, give it a โค๏ธ and follow for more React Native deep dives!
Top comments (0)