Bug(push): Duplicate notifications from SW polling/SSE & accumulative PushSubscriptions causing "too many subscriptions" #375

Closed
opened 2026-09-16 14:42:46 +00:00 by mbjboon82 · 2 comments
  1. Duplicate & Ghost Notifications (Service Worker Event Overlap)

In the compiled Service Worker (sw.js), self.registration.showNotification is triggered across three distinct flows rather than solely through the native push event:

  • Web Push Listener: Standard push event handler.
  • Polling/Sync Handler: Fires independently on background sync cycles.
  • EventSource / JMAP SSE Listener: Triggers whenever JMAP mailbox mutations occur (e.g., marking emails as read or moving messages).

Because there is no centralized state tracking or deduplication logic between these handlers, incoming emails often trigger up to three identical notifications. Furthermore, non-incoming state updates generate "ghost" notifications when triage actions are performed on another client.

Expected Behavior:

Notifications should be driven exclusively by the Web Push listener (or strictly filtered by checking if the unread count increased), ensuring a single notification per incoming email.


  1. Unhandled JMAP PushSubscription Accumulation

When push notifications are re-enabled, or when a user clears their browser cache or reinstall the PWA, a new Web Push endpoint is generated. The frontend registers this endpoint using a JMAP PushSubscription/set create payload.

However, the client does not query existing subscriptions (PushSubscription/get) or prune stale tokens (destroy) beforehand. On strict RFC 8620-compliant JMAP servers (such as Stalwart), stale tokens quickly accumulate until the account hits the server's subscription threshold, returning "Error: too many subscriptions".

Expected Behavior:

Prior to issuing a PushSubscription/set create request, the client should fetch active subscriptions, match by deviceClientId, and issue a destroy for stale endpoints.


JMAP Server: Stalwart Mail Server v0.16.21
Client App: ihasmail PWA
Browser/OS: Android / Chrome PWA

Rebuilt from: GH Archive, notification email, session transcript.

1) Duplicate & Ghost Notifications (Service Worker Event Overlap) In the compiled Service Worker (sw.js), self.registration.showNotification is triggered across three distinct flows rather than solely through the native push event: - Web Push Listener: Standard push event handler. - Polling/Sync Handler: Fires independently on background sync cycles. - EventSource / JMAP SSE Listener: Triggers whenever JMAP mailbox mutations occur (e.g., marking emails as read or moving messages). Because there is no centralized state tracking or deduplication logic between these handlers, incoming emails often trigger up to three identical notifications. Furthermore, non-incoming state updates generate "ghost" notifications when triage actions are performed on another client. Expected Behavior: Notifications should be driven exclusively by the Web Push listener (or strictly filtered by checking if the unread count increased), ensuring a single notification per incoming email. ------------------ 2) Unhandled JMAP PushSubscription Accumulation When push notifications are re-enabled, or when a user clears their browser cache or reinstall the PWA, a new Web Push endpoint is generated. The frontend registers this endpoint using a JMAP PushSubscription/set create payload. However, the client does not query existing subscriptions (PushSubscription/get) or prune stale tokens (destroy) beforehand. On strict RFC 8620-compliant JMAP servers (such as Stalwart), stale tokens quickly accumulate until the account hits the server's subscription threshold, returning "Error: too many subscriptions". Expected Behavior: Prior to issuing a PushSubscription/set create request, the client should fetch active subscriptions, match by deviceClientId, and issue a destroy for stale endpoints. --------------------- JMAP Server: Stalwart Mail Server v0.16.21 Client App: ihasmail PWA Browser/OS: Android / Chrome PWA <sub>Rebuilt from: GH Archive, notification email, session transcript.</sub>
Owner

Thanks for the detailed report. Both problems were real, though the causes turned out a little different from the ones you suspected.

Duplicate and "ghost" notifications

The service worker only shows notifications from the push event; there's no polling or SSE path in it. The problem was what ihasmail subscribed to. It asked Stalwart for every change to your mail, so marking a message read or moving it on another device arrived as a push, and the worker could only show it as "New mail". It now subscribes to new deliveries only.

Also changed:

  • It no longer notifies while ihasmail is open and focused.
  • An open tab now leaves notifications to the service worker, so the two can't double up.
  • Notifications now carry the message itself. Archive and Mark as read work, and tapping one opens the message.

"too many subscriptions"

We tested this against a live Stalwart 0.16.22:

  • Registering again with the same device id doesn't replace the old subscription; Stalwart keeps both.
  • It allows 15 subscriptions per account.

ihasmail registered again on every renewal, so subscriptions piled up until the account hit that limit. Now it:

  • extends its existing subscription instead of registering a new one;
  • removes its own duplicates;
  • clears out a stale subscription to make room if the account is already full.

When ihasmail's server runs push in subscribe mode, it now also removes the subscriptions its previous run left behind.

Fixed in #389, and it'll be in the next release. Once you've upgraded, opening ihasmail on each device is enough to tidy up. Any leftovers from devices you no longer use expire on their own within 7 days.

Thanks for the detailed report. Both problems were real, though the causes turned out a little different from the ones you suspected. **Duplicate and "ghost" notifications** The service worker only shows notifications from the `push` event; there's no polling or SSE path in it. The problem was what ihasmail subscribed to. It asked Stalwart for every change to your mail, so marking a message read or moving it on another device arrived as a push, and the worker could only show it as "New mail". It now subscribes to new deliveries only. Also changed: - It no longer notifies while ihasmail is open and focused. - An open tab now leaves notifications to the service worker, so the two can't double up. - Notifications now carry the message itself. Archive and Mark as read work, and tapping one opens the message. **"too many subscriptions"** We tested this against a live Stalwart 0.16.22: - Registering again with the same device id doesn't replace the old subscription; Stalwart keeps both. - It allows 15 subscriptions per account. ihasmail registered again on every renewal, so subscriptions piled up until the account hit that limit. Now it: - extends its existing subscription instead of registering a new one; - removes its own duplicates; - clears out a stale subscription to make room if the account is already full. When ihasmail's server runs push in subscribe mode, it now also removes the subscriptions its previous run left behind. Fixed in #389, and it'll be in the next release. Once you've upgraded, opening ihasmail on each device is enough to tidy up. Any leftovers from devices you no longer use expire on their own within 7 days.
Author

Thanks, really appreciate all the work and the incredibly quick fixes!

Op wo 16 sep 2026, 20:43 schreef jcoffey @.***>:

jcoffey-dev left a comment (Coffey-Labs/ihasmail#375)
https://github.com/Coffey-Labs/ihasmail/issues/375#issuecomment-5702724442

Thanks for the detailed report. Both problems were real, though the causes
turned out a little different from the ones you suspected.

Duplicate and "ghost" notifications

The service worker only shows notifications from the push event; there's
no polling or SSE path in it. The problem was what ihasmail subscribed to.
It asked Stalwart for every change to your mail, so marking a message read
or moving it on another device arrived as a push, and the worker could only
show it as "New mail". It now subscribes to new deliveries only.

Also changed:

  • It no longer notifies while ihasmail is open and focused.
  • An open tab now leaves notifications to the service worker, so the
    two can't double up.
  • Notifications now carry the message itself. Archive and Mark as read
    work, and tapping one opens the message.

"too many subscriptions"

We tested this against a live Stalwart 0.16.22:

  • Registering again with the same device id doesn't replace the old
    subscription; Stalwart keeps both.
  • It allows 15 subscriptions per account.

ihasmail registered again on every renewal, so subscriptions piled up
until the account hit that limit. Now it:

  • extends its existing subscription instead of registering a new one;
  • removes its own duplicates;
  • clears out a stale subscription to make room if the account is
    already full.

When ihasmail's server runs push in subscribe mode, it now also removes
the subscriptions its previous run left behind.

Fixed in #389 https://github.com/Coffey-Labs/ihasmail/pull/389, and
it'll be in the next release. Once you've upgraded, opening ihasmail on
each device is enough to tidy up. Any leftovers from devices you no longer
use expire on their own within 7 days.


Reply to this email directly, view it on GitHub
https://github.com/Coffey-Labs/ihasmail/issues/375?email_source=notifications&email_token=CNVKZN75OGIUV56AIYOBC335PLNNDA5CNFSNUABFM5UWIORPF5TWS5BNNB2WEL2JONZXKZKDN5WW2ZLOOQXTKNZQGI3TENBUGQZKM4TFMFZW63VGMF2XI2DPOKSWK5TFNZ2KYZTPN52GK4S7MNWGSY3L#issuecomment-5702724442,
or unsubscribe
https://github.com/notifications/unsubscribe-auth/CNVKZN2SGMFKGYYGTN4ZUDL5PLNNDAVCNFSNUABGKJSXA33TNF2G64TZHMYTANZZGEYTKNRZGE5US43TOVSTWNJUG42TSMRYGM4TRILWAI
.
Triage notifications, keep track of coding agent tasks and review pull
requests on the go with GitHub Mobile for iOS
https://github.com/notifications/mobile/ios/CNVKZN2MROHHOEEVH6ZRY6D5PLNNDA5CNFSNUABFM5UWIORPF5TWS5BNNB2WEL2JONZXKZKDN5WW2ZLOOQXTKNZQGI3TENBUGQZKM4TFMFZW63VGMF2XI2DPOKSWK5TFNZ2KUZTPN52GK4S7NFXXG
and Android
https://github.com/notifications/mobile/android/CNVKZN7WB3VFSO2MNJCDSST5PLNNDA5CNFSNUABFM5UWIORPF5TWS5BNNB2WEL2JONZXKZKDN5WW2ZLOOQXTKNZQGI3TENBUGQZKM4TFMFZW63VGMF2XI2DPOKSWK5TFNZ2K4ZTPN52GK4S7MFXGI4TPNFSA.
Download it today!
You are receiving this because you authored the thread.Message ID:
@.***>

Thanks, really appreciate all the work and the incredibly quick fixes! Op wo 16 sep 2026, 20:43 schreef jcoffey ***@***.***>: > *jcoffey-dev* left a comment (Coffey-Labs/ihasmail#375) > <https://github.com/Coffey-Labs/ihasmail/issues/375#issuecomment-5702724442> > > Thanks for the detailed report. Both problems were real, though the causes > turned out a little different from the ones you suspected. > > *Duplicate and "ghost" notifications* > > The service worker only shows notifications from the push event; there's > no polling or SSE path in it. The problem was what ihasmail subscribed to. > It asked Stalwart for every change to your mail, so marking a message read > or moving it on another device arrived as a push, and the worker could only > show it as "New mail". It now subscribes to new deliveries only. > > Also changed: > > - It no longer notifies while ihasmail is open and focused. > - An open tab now leaves notifications to the service worker, so the > two can't double up. > - Notifications now carry the message itself. Archive and Mark as read > work, and tapping one opens the message. > > *"too many subscriptions"* > > We tested this against a live Stalwart 0.16.22: > > - Registering again with the same device id doesn't replace the old > subscription; Stalwart keeps both. > - It allows 15 subscriptions per account. > > ihasmail registered again on every renewal, so subscriptions piled up > until the account hit that limit. Now it: > > - extends its existing subscription instead of registering a new one; > - removes its own duplicates; > - clears out a stale subscription to make room if the account is > already full. > > When ihasmail's server runs push in subscribe mode, it now also removes > the subscriptions its previous run left behind. > > Fixed in #389 <https://github.com/Coffey-Labs/ihasmail/pull/389>, and > it'll be in the next release. Once you've upgraded, opening ihasmail on > each device is enough to tidy up. Any leftovers from devices you no longer > use expire on their own within 7 days. > > — > Reply to this email directly, view it on GitHub > <https://github.com/Coffey-Labs/ihasmail/issues/375?email_source=notifications&email_token=CNVKZN75OGIUV56AIYOBC335PLNNDA5CNFSNUABFM5UWIORPF5TWS5BNNB2WEL2JONZXKZKDN5WW2ZLOOQXTKNZQGI3TENBUGQZKM4TFMFZW63VGMF2XI2DPOKSWK5TFNZ2KYZTPN52GK4S7MNWGSY3L#issuecomment-5702724442>, > or unsubscribe > <https://github.com/notifications/unsubscribe-auth/CNVKZN2SGMFKGYYGTN4ZUDL5PLNNDAVCNFSNUABGKJSXA33TNF2G64TZHMYTANZZGEYTKNRZGE5US43TOVSTWNJUG42TSMRYGM4TRILWAI> > . > Triage notifications, keep track of coding agent tasks and review pull > requests on the go with GitHub Mobile for iOS > <https://github.com/notifications/mobile/ios/CNVKZN2MROHHOEEVH6ZRY6D5PLNNDA5CNFSNUABFM5UWIORPF5TWS5BNNB2WEL2JONZXKZKDN5WW2ZLOOQXTKNZQGI3TENBUGQZKM4TFMFZW63VGMF2XI2DPOKSWK5TFNZ2KUZTPN52GK4S7NFXXG> > and Android > <https://github.com/notifications/mobile/android/CNVKZN7WB3VFSO2MNJCDSST5PLNNDA5CNFSNUABFM5UWIORPF5TWS5BNNB2WEL2JONZXKZKDN5WW2ZLOOQXTKNZQGI3TENBUGQZKM4TFMFZW63VGMF2XI2DPOKSWK5TFNZ2K4ZTPN52GK4S7MFXGI4TPNFSA>. > Download it today! > You are receiving this because you authored the thread.Message ID: > ***@***.***> >
This repo is archived. You cannot comment on issues.