The Day I Chased a “Ghost” Bug in Real-Time Notifications
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
- Register the listener once at app startup (e.g., in
main()or a top‑level provider) and keep it in a single, centralized place. - 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.