Preventing Missing FCM Notifications in Mobile Apps

Summary

When using Firebase Cloud Messaging (FCM) on mobile devices, the way a message payload is structured determines whether your app’s code runs when the notification is received.

  • Data‑only messages fire OnMessageReceived() only when the app is in the foreground or has a background service active.
  • Notification + data messages allow the OS to display the notification automatically, but the data payload is delivered only when the user taps the notification.

For an application that relies on a local record of every notification (e.g., a Realm table for an older‑age user base) this distinction causes a “missing‑notification” problem: notifications appear, but the local log remains incomplete.

Root Cause

  • FCM’s notification messages are handled by the system tray.
    • The OS shows the notification without giving the app a chance to process the payload.
    • OnMessageReceived() is skipped if the app is terminated or swiped away.
  • Data‑only messages survive only when the app is alive (foreground/background).
    • Swiping the app away terminates the process, so the data payload is never processed.

This mismatch between how the OS handles the message and when your code runs is the root cause of data loss in the local store.

Why This Happens in Real Systems

  • OS Optimizations
    • Android and iOS aggressively kill background processes to conserve battery, which discards data‑only messages.
    • Notification messages are considered low-risk and are shown by the OS to guarantee user reachability.
  • App Lifecycle Management
    • Developers often rely on implicit background services or short‑lived workers that do not persist when the user swipes the app away.
  • Performance Trade‑offs
    • Data‑only messages require a network call even for a simple log entry, so many teams disable them to save bandwidth and reduce load on the device.

Real-World Impact

  • Users see the notification but do not see it in the app’s notification log.
  • Analytics that depend on the local log become inaccurate.
  • Support requests surge: “I received a notification but it isn’t listed.”
  • Older‑age users, who may not examine their notification list, miss vital reminders because they rely on the log to keep a record.

Example or Code

// Example FCM message payload (single notification + data)
{
  "to": "",
  "priority": "high",
  "content_available": true,
  "notification": {
    "title": "Appointment Reminder",
    "body": "Your visit is in 30 minutes."
  },
  "data": {
    "appointment_id": "12345",
    "action": "log"
  }
}
// On Android in a FirebaseMessagingService
public override void OnMessageReceived(RemoteMessage message)
{
    // This method is **not** called for pure notification messages
    var apptId = message.Data.ContainsKey("appointment_id") ? message.Data["appointment_id"] : null;
    if (apptId != null)
    {
        // Persist to Realm
        realm.Write(() => realm.CreateOrUpdate(new Appointment { Id = apptId, /* ... */ }));
    }
}

How Senior Engineers Fix It

  1. Instrument every message with a unique ID (UUID).
  2. Send a lightweight data‑only “heartbeat” ping from the app at regular intervals (e.g., every 24 h) to confirm receipt of pending notifications.
  3. Use a foreground service or WorkManager (Android) to run a short background task that:
    • Checks locally for unrecorded notifications.
    • Requests the FCM server for missing data via a custom REST endpoint (if feasible).
  4. Persist the notification payload in the OS‑generated notification by attaching the ID in the content-available field, then use a BroadcastReceiver that triggers on notification dismissal to remove orphan entries.
  5. Leverage the content_available flag for low‑priority data bursts so the app wakes up silently to sync the missed notifications when it next resumes.

Key takeaway: Never rely solely on the OS to deliver data; always ensure your app has a deterministic path to reconcile state, either through a minimal background worker or a lightweight sync service.

Why Juniors Miss It

  • They assume the payload that shows in the notification is automatically delivered inside the app.
  • They overlook the app lifecycle and think “notification + data” means OnMessageReceived() will fire always.
  • They ignore the trade‑off between performance and reliability, believing higher priority automatically guarantees delivery.
  • They are unfamiliar with platform‐specific background execution limits (Android Doze, iOS background fetch).

By following the patterns above, senior engineers maintain a consistent, reliable notification log without relying on cloud storage, ensuring that users—especially those in older age groups—receive the reminders they need.

Leave a Comment