Skip to content

ADR-0072: Prevent notification storm on Android#4381

Open
dab246 wants to merge 3 commits intomasterfrom
bugfix/tf-4366-add-adr-for-storm-of-notification-in-android
Open

ADR-0072: Prevent notification storm on Android#4381
dab246 wants to merge 3 commits intomasterfrom
bugfix/tf-4366-add-adr-for-storm-of-notification-in-android

Conversation

@dab246
Copy link
Member

@dab246 dab246 commented Mar 13, 2026

Add ADR-0072: Prevent notification storm on Android

Summary

This PR introduces a new Architecture Decision Record (ADR) that defines how the application prevents notification storms on Android when a device reconnects after a long idle period.

The ADR documents the root causes of the issue and proposes several defensive mechanisms to stabilize the push notification system and limit excessive local notifications.


Related Issue

Fixes #4366


Problem

Some Android users experience a large burst of notifications when their device reconnects to the network after being idle (for example overnight).

When reconnection occurs, Firebase Cloud Messaging (FCM) may deliver many push events within a short time window.

Each push currently triggers the full notification pipeline and may generate multiple local notifications.

Example scenario:

100 push events at T0
100 push events at T0 + 31 seconds

This can lead to:

  • notification flooding
  • hundreds of local notifications
  • Android notification service overload
  • device UI freezing for several minutes
  • degraded user experience

Decision

To mitigate this issue, the ADR proposes four defensive mechanisms.


1. Push debounce in background handler

A debounce window is introduced in the background push handler to prevent repeated push bursts from triggering the notification pipeline multiple times.

When multiple FCM messages arrive within a short period of time:

  • only the first push within the debounce window is processed
  • subsequent pushes within the window are ignored

This prevents push bursts from repeatedly executing the full notification pipeline.


2. Notification limiter

When a push burst is processed, the application limits the number of generated notifications.

Rules:

  • maximum 20 notifications
  • notifications are generated only for Inbox emails
  • emails requiring user action (needs-action) are prioritized
  • remaining emails are selected by newest first

Selection priority:

1. actionable emails (needs-action)
2. newest emails (Inbox only)

Emails delivered to other mailboxes (Sent, Drafts, Spam, Trash, etc.) will not generate notifications.

This significantly reduces unnecessary notifications during large synchronization bursts.


3. Foreground delivery state synchronization

The email delivery state is currently stored when push notifications are processed.

To maintain state consistency, the delivery state will also be updated during foreground synchronization when the email state cache is refreshed.

This ensures the latest delivery state is always persisted even if updates originate outside the push pipeline.


4. Fix local notification removal error

The notification cancellation logic will be improved to safely handle failures.

Sentry reports the following error:

PlatformException: Missing type parameter
FlutterLocalNotificationsPlugin.removeNotificationFromCache

When cancellation fails:

  • stale notifications may remain active
  • notification grouping becomes inconsistent
  • notification count increases

The updated logic introduces defensive cancellation handling and safer notification removal.


Expected Benefits

This design provides several improvements:

  • prevents Android notification storms
  • guarantees a maximum of 20 notifications per push burst
  • reduces unnecessary notifications from non-Inbox mailboxes
  • stabilizes notification behavior during device reconnection
  • reduces Android notification service overload
  • improves overall user experience

Scope

This PR adds documentation only.

The ADR defines the architecture and design decisions.
Implementation will be delivered in follow-up issues and pull requests.


Files Added

docs/adr/0072-prevent-notification-storm-android.md

Summary by CodeRabbit

  • Documentation
    • Added architecture guidance for Android notification handling: debounce duplicate pushes, cap and prioritize notifications to reduce bursts, persist delivery state during sync, and safely handle notification removal to avoid failures.
  • Bug Fixes
    • Reduced duplicate and excessive notifications and improved reliability of notification updates.

@coderabbitai
Copy link

coderabbitai bot commented Mar 13, 2026

Walkthrough

Adds an ADR that defines four defensive mechanisms to prevent Android notification storms: a 30-second push debounce persisted via SharedPreferences; a notification limiter that caps generated notifications at 20 and prioritizes action-required then newest Inbox emails with a selection algorithm; persisting email delivery state during foreground synchronization via FCMCacheManager.storeStateToRefresh; and safe local notification removal wrapped in try/catch. Describes architecture flow updates, integration points in push and email handling code, example Dart implementations, and future improvement options.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately and concisely summarizes the main change: documentation of an ADR addressing Android notification storms.
Linked Issues check ✅ Passed The PR addresses all coding-related requirements from #4366: documents four defensive mechanisms (debounce, notification limiting, state synchronization, and safe removal) to prevent notification storms and includes example implementations.
Out of Scope Changes check ✅ Passed All changes are within scope—the single file addition (ADR documentation) directly addresses the notification storm problem outlined in issue #4366 with no unrelated modifications.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch bugfix/tf-4366-add-adr-for-storm-of-notification-in-android
📝 Coding Plan
  • Generate coding plan for human review comments

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@docs/adr/0072-prevent-notification-storm-android.md`:
- Line 97: Hyphenate the numeric adjective in the sentence "The background push
handler introduces a 30 second debounce window" by changing "30 second" to
"30-second" so it reads "The background push handler introduces a 30-second
debounce window"; locate the exact sentence in the ADR text and update it
accordingly.
- Line 17: Update every fenced code block in the ADR document that currently
uses bare backticks to include a language identifier (e.g., change ``` to
```text). Specifically, replace the unnamed fences that enclose examples like
the block with "100 push events at T0" and the other unnamed examples throughout
the ADR so they become labeled (e.g., ```text) to satisfy markdownlint MD040 and
improve readability.
- Around line 261-272: The code is saving the email cache state
(StateType.email) then incorrectly reusing newState when storing the delivery
cursor to FCM; update the FCMCacheManager.storeStateToRefresh call to pass the
delivery-state token (e.g., deliveryNewState or deliveryCursor) for
TypeName.emailDelivery rather than newState so the delivery stream and email
stream remain separate; locate the calls to stateCacheManager.saveState and
FCMCacheManager.storeStateToRefresh and replace the second argument value for
the TypeName.emailDelivery call with the correct delivery-state variable
returned/produced by your email-delivery processing logic.
- Around line 106-117: The check-then-set in shouldProcessPush allows a race
where multiple handlers read the old 'last_push_processed_time' before any
write, so two handlers can both proceed; fix it by performing a check, writing
the candidate timestamp, then re-reading to confirm you won the race: read
prefs.getInt('last_push_processed_time'), if within debounce return false; else
compute now, await prefs.setInt('last_push_processed_time', now), then read back
prefs.getInt('last_push_processed_time') and only return true if that stored
value equals now (otherwise return false) so only the first writer proceeds.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 9708a519-1aa6-411c-aead-c32446de4d66

📥 Commits

Reviewing files that changed from the base of the PR and between e2c158d and fd14b4f.

📒 Files selected for processing (1)
  • docs/adr/0072-prevent-notification-storm-android.md

@dab246 dab246 changed the title TF-4366 ADR-0072: Prevent notification storm on Android ADR-0072: Prevent notification storm on Android Mar 13, 2026



# 1. Push debounce in background handler (30s window)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I propose the following algorithm, not a normal debounce

WHEN receiving FCM call with emailDelivery wait 1 minute, ignore received message during this 1 minute, then and only then fire notifs for the last 1 minute.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The proposed algorithm is closer to a batch aggregation window rather than a debounce. However, the FCM background handler runs inside a short-lived Flutter isolate and is not designed for long-running tasks. Delaying notification processing for 1 minute would require keeping the isolate alive, which is not guaranteed by the Android system. Therefore the ADR proposes a simpler debounce mechanism that drops repeated push events within a 30s window while still allowing the notification pipeline to run immediately for the first event.


Rules:

* maximum **20 notifications**
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍 Good idea




# Action-required Email Detection
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good idea but let's not implement this for now IMO

Selection priority:

1. actionable emails (`needs-action`)
2. newest emails
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  1. Favour INBOX, ignore SPAM / Trash

}
```

This ensures notification removal failures do not interrupt the notification pipeline.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

+1 !

Copy link
Member

@chibenwa chibenwa left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Small adjustments but I mostly agree with this document.

And getting the ADR before writing code is truely nice. THANKS for making the effort.

```dart
Future<void> handleFirebaseBackgroundMessage(RemoteMessage message) async {

if (!await shouldProcessPush()) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

please check the posibility of this in isolated in flutter

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It work well

@dab246 dab246 requested review from chibenwa and hoangdat March 13, 2026 09:29
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@docs/adr/0072-prevent-notification-storm-android.md`:
- Line 126: Change the "Background execution constraints" heading from level-3
to level-2 (replace "### Background execution constraints" with "## Background
execution constraints") so it directly follows the document's level-1 section
and ensures headings increment by one; also scan nearby headings in the ADR to
confirm no other headings skip a level and adjust them similarly.
- Around line 199-201: The example uses a hard-coded "inbox-id"; update the
isInboxEmail guidance to explain how to derive the real inbox ID for an account
(e.g., inspect the account's mailbox objects and find the mailbox whose
mailboxRole == MailboxRole.inbox or equivalent) and then check that ID against
email.mailboxIds in isInboxEmail (function isInboxEmail, type PresentationEmail,
property mailboxIds); mention that implementers should resolve the inbox mailbox
ID from the account's mailbox list rather than using a literal string.
- Around line 217-251: selectEmailsForNotification currently collects all
actionEmails uncapped and then trims the combined list only at the end, which
can let action emails monopolize the 20 slots; change the logic so actionEmails
is capped to at most maxNotifications (use actionEmails.take(maxNotifications)),
compute remainingSlots = maxNotifications - cappedActionCount, then fill
remainingSlots from newestInboxEmails (constructed from the heap) and return
[...cappedActionEmails, ...newestInboxEmailsTaken] so the total never exceeds
maxNotifications and inbox slots are preserved when actions are abundant; keep
references to actionEmails, heap, newestInboxEmails, and maxNotifications when
making these changes.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 5b2f0032-7c08-4f07-bade-a1851ff36e05

📥 Commits

Reviewing files that changed from the base of the PR and between fd14b4f and 817be3a.

📒 Files selected for processing (1)
  • docs/adr/0072-prevent-notification-storm-android.md

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
docs/adr/0072-prevent-notification-storm-android.md (1)

94-107: Note potential race condition in debounce implementation.

The shouldProcessPush() example uses a check-then-set pattern: reading lastTime (line 97) and writing the new timestamp (line 104) are separate operations. If two FCM background handlers execute concurrently, both may read the old timestamp before either writes, allowing both to proceed and bypass the debounce.

This race is low-probability (requires FCM isolates spawning within milliseconds of each other), but the ADR should either:

  1. Acknowledge this as an acceptable edge case for "best-effort" debouncing, or
  2. Propose a mitigation (e.g., atomic check-and-set via native code, file locking, or a check-write-verify pattern)

Given that this is an architectural proposal, consider adding a brief note in the ADR clarifying the atomicity guarantees (or lack thereof) of this approach.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@docs/adr/0072-prevent-notification-storm-android.md` around lines 94 - 107,
The debounce implementation in shouldProcessPush() has a potential race because
it reads and then writes SharedPreferences separately; acknowledge this or
propose a mitigation. Update the ADR text near the shouldProcessPush() example
to either state that this is a best-effort, non-atomic debounce with possible
rare duplicates, or describe a mitigation such as using an atomic check-and-set
via native code or file-locking, or implementing a check-write-verify loop that
re-reads the stored last_push_processed_time after writing and aborts if another
handler has advanced it; reference shouldProcessPush() and the
last_push_processed_time key in the note so readers can find the code and
understand the atomicity guarantees (or lack thereof).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@docs/adr/0072-prevent-notification-storm-android.md`:
- Around line 94-107: The debounce implementation in shouldProcessPush() has a
potential race because it reads and then writes SharedPreferences separately;
acknowledge this or propose a mitigation. Update the ADR text near the
shouldProcessPush() example to either state that this is a best-effort,
non-atomic debounce with possible rare duplicates, or describe a mitigation such
as using an atomic check-and-set via native code or file-locking, or
implementing a check-write-verify loop that re-reads the stored
last_push_processed_time after writing and aborts if another handler has
advanced it; reference shouldProcessPush() and the last_push_processed_time key
in the note so readers can find the code and understand the atomicity guarantees
(or lack thereof).

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: d8fdb336-27cf-4d0d-89e4-3e21f0cf6cf8

📥 Commits

Reviewing files that changed from the base of the PR and between 817be3a and 48211a3.

📒 Files selected for processing (1)
  • docs/adr/0072-prevent-notification-storm-android.md

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Storm of notification in Android v0.24.14

3 participants