DEV Community

Cover image for I Built a 24/7 Background Service in React Native That Survives Everything (Even After Swiping Away the Notification!)
Bhupender Singh Mehta
Bhupender Singh Mehta

Posted on

I Built a 24/7 Background Service in React Native That Survives Everything (Even After Swiping Away the Notification!)

๐ŸŽฏ 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
  }
});
Enter fullscreen mode Exit fullscreen mode

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);
});
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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();
  }
}
Enter fullscreen mode Exit fullscreen mode

๐Ÿคฏ 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;
}
Enter fullscreen mode Exit fullscreen mode

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();
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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);
}
Enter fullscreen mode Exit fullscreen mode

๐Ÿ“‹ Required Setup

AndroidManifest.xml

<manifest>
  <uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
  <uses-permission android:name="android.permission.WAKE_LOCK" />
</manifest>
Enter fullscreen mode Exit fullscreen mode

Usage in Your App

// Start on login
await BackgroundSyncService.init();

// Stop on logout
await BackgroundSyncService.stop();
Enter fullscreen mode Exit fullscreen mode

๐Ÿงช 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!
Enter fullscreen mode Exit fullscreen mode

โœ… DO: Use Reasonable Intervals

await this.sleep(120000); // 2 minutes is fine for most use cases
Enter fullscreen mode Exit fullscreen mode

โŒ DON'T: Hit Your API on Every Check

// Bad: Always calling API
await sendToAPI(getAllData());
Enter fullscreen mode Exit fullscreen mode

โœ… DO: Only Send New Data

// Good: Check locally first
const newData = await getNewData();
if (newData.length > 0) {
  await sendToAPI(newData);
}
Enter fullscreen mode Exit fullscreen mode

๐ŸŽฏ 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:

  1. It IS a real foreground service - despite the swipeable notification
  2. Modern Android allows dismissing notifications - this is by design
  3. Optimize your check intervals - balance responsiveness with battery life
  4. 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


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)