Your retry logic fires. The user gets the same notification twice. They think your app is broken. They’re not wrong.

The Problem with Retries#

Push delivery is at-least-once by design. Your server sends to APNs/FCM, the network hiccups, you don’t get a response, so you retry. APNs might have delivered the first one. The user now sees two identical alerts.

The fix lives at two levels: your server and the gateway.

Deduplication Keys#

At the server level, assign a deduplication key to each notification before it enters the delivery pipeline. The key should encode what the notification is about: user ID, event type, entity ID, and a time window.

String dedupKey = userId + ":" + eventType + ":" + entityId + ":" + windowBucket;

Store this key in Redis with a TTL. Before sending, check if it exists. If it does, skip. This prevents your own retry logic from firing duplicates.

Collapse Keys at the Gateway#

APNs has apns-collapse-id and FCM has collapse_key. When you set this header, the gateway replaces any pending undelivered notification with the same collapse ID instead of queuing a second one. The device gets at most one notification per collapse key. Useful for things like badge updates: “you have 5 unread messages” supersedes “you have 4 unread messages.”

This doesn’t help if the first notification was already delivered. But for retries happening within seconds of the original, it prevents most duplicates.

At Salesforce#

We had an approval notification that fired from two services during a code migration where both the old and new path were briefly live. Same user, same approval request, two pushes. The issue ran for about 48 hours before we caught it from support tickets. Adding a dedup key in Redis with a 10-minute TTL stopped the duplicate. We also backfilled collapse keys on all approval notifications as a second layer.

What I’m Learning#

Idempotency at the push layer is the same concept as idempotency in payment processing: the operation should produce the same outcome no matter how many times you call it.

Have you ever debugged a duplicate notification without a dedup key in place? How long did it take to find?