The Day I Chased a “Ghost” Bug in Real-Time Notifications

Published: (March 23, 2026 at 02:20 AM EDT)
3 min read
Source: Dev.to

Source: Dev.to

The Problem That Wasn’t There… or Was It?

A few months ago I was working on a mobile app with real‑time notifications. The feature seemed straightforward: whenever a new order was placed in the e‑commerce app, the user should receive a push notification instantly.

Everything worked perfectly during development—on my devices notifications popped up within milliseconds. But when beta testers started using the app, something strange happened: some users received duplicate notifications. It felt like chasing a ghost. How could the system behave so inconsistently?

The app used Firebase Cloud Messaging (FCM) for push notifications. My initial suspects were the backend, the mobile framework, or even the users’ devices. I went step by step:

  • Checked the backend logs—​all notifications were sent correctly.
  • Concluded the problem was platform‑specific and subtle.

After hours of debugging I realized the culprit: multiple Firebase listeners were being registered unintentionally.

Root Cause

In my Flutter code I had something like:

// Example listener registration (simplified)
FirebaseMessaging.onMessage.listen((RemoteMessage message) {
  // Handle incoming notification
  showNotification(message);
});

This listener was being registered every time a user navigated to the home screen. Each navigation added a new listener, so if a user navigated back and forth three times, the same notification would trigger three times. Additionally, race conditions sometimes caused the listener to fire before the app was fully initialized, resulting in delayed or dropped notifications.

Solution

  1. Register the listener once at app startup (e.g., in main() or a top‑level provider) and keep it in a single, centralized place.
  2. Make showNotification() idempotent so that even if the same message arrives multiple times, the app displays it only once.
// Register once, e.g., in main()
void main() {
  WidgetsFlutterBinding.ensureInitialized();
  FirebaseMessaging.onMessage.listen(_handleMessage);
  runApp(MyApp());
}

// Centralized handler
void _handleMessage(RemoteMessage message) {
  showNotification(message);
}

// Idempotent notification display
final Set _displayedMessageIds = {};

void showNotification(RemoteMessage message) {
  final String id = message.messageId ?? '';
  if (id.isEmpty || _displayedMessageIds.contains(id)) return;

  _displayedMessageIds.add(id);
  // TODO: Show the notification using your preferred method
}

Takeaways

  • Ghost bugs often stem from subtle lifecycle issues.
  • In real‑time systems, always consider how many listeners exist, when they fire, and what happens if the user navigates around your app.
  • Centralizing asynchronous event handling can prevent duplicate actions and make debugging far easier.

For developers working with notifications, async events, or real‑time systems: think carefully about listener registration and lifecycle management—it can save hours of chasing ghosts.

0 views
Back to Blog

Related posts

Read more »