I'm seeing a timestamp display issue on Apple Watch Notification Center, and I'd like to confirm whether this is a known watchOS behavior or whether there's a setup mistake on our side.
Symptom
The same APNs notification displays the correct time on iPhone Notification Center and on the initial Apple Watch banner. After the Watch screen turns off and the user later opens Notification Center on Apple Watch, the same notification may show an incorrect relative timestamp such as "3 hours ago" or even "yesterday". The drift is per-notification and persists for that notification until it's dismissed.
iPhone NC always shows the correct time. WhatsApp tested side-by-side on the same iPhone/Watch pair does not show this drift.
Setup
- iOS 26.4.2 on iPhone 16 Pro
- watchOS 26.4 on Apple Watch Series 10
- App with paired Apple Watch
- A
UNNotificationServiceExtensionthat decrypts E2EE message previews and applies Communication Notification enrichment viaINSendMessageIntentandcontent.updating(from:) - Production APNs environment, TestFlight builds
- No beta software
Isolation tests already performed
| Minimal APNs (alert + sound only) | no | no | no drift |
NSE skips content.updating(from:), INInteraction.donate/delete (still calls them as no-op via diagnostic build) | yes | yes — modifies content | drift |
NSE bypasses ALL Intents/Communication Notification APIs (no INPerson, no INSendMessageIntent, no avatar, no updating(from:)); just modifies title/body/sound/category and returns the mutable copy | yes | yes — modifies content | drift |
| Production-like APNs payload (thread-id, target-content-id, category, sound, badge, custom userInfo) but WITHOUT mutable-content | no | no | no drift |
Eliminated as causes: content.updating(from:), INSendMessageIntent, INInteraction.donate, INInteraction.delete(with:), INPerson/INPersonHandle (not even constructed in test 3), avatar fetching, thread-id, target-content-id, category, sound, badge, custom userInfo, custom createdAt timestamp, stale Siri/Apple Intelligence history (cleared manually on iPhone and Watch).
The pattern
The only consistent variable distinguishing the no-drift cases from the drift cases is whether mutable-content: 1 is set on the APNs payload (i.e. whether the UNNotificationServiceExtension is invoked). Once invoked, the extension's behavior with respect to Communication Notifications does not seem to affect the outcome — the drift reproduces even when the NSE only modifies title/body/sound and returns.
Questions
- Is there a known watchOS behavior where notifications processed by a
UNNotificationServiceExtensionuse a different timestamp source on Apple Watch Notification Center after the Watch screen has been turned off and reopened, while the initial Watch banner and iPhone Notification Center show the correct delivery time? - Are there specific
UNMutableNotificationContentproperties or APNs payload flags that should be preserved (or avoided) when returning content from an NSE to keep the Watch NC timestamp consistent with the delivery time? - For E2EE messaging apps, is there a recommended pattern to decrypt and return content from an NSE that avoids this drift on watchOS?
Happy to provide an anonymized snippet of NotificationService.swift and the APNs payload format if useful.
Thanks.
I'm seeing a timestamp display issue on Apple Watch Notification Center, and I'd like to confirm whether this is a known watchOS behavior or whether there's a setup mistake on our side.
I'm not aware of any issue like this, and the general behavior you’re describing sounds like a bug. Have you filed a bug on this and, if so, what's the bug number? Also, what system versions are you seeing this on and have you seen it on watchOS 26.5?
For E2EE messaging apps, is there a recommended pattern to decrypt and return content from an NSE that avoids this drift on watchOS?
In theory, what you're doing should "just work". More specifically, by design, your extension doesn't control the time data of the notification you're returning, so what actually happens is that the UNNotificationContent your extension returned is immediately embedded into a newly created UNNotification object, which is then processed "normally". How that would end up creating the situation you're describing is something I can't explain.
However, this is interesting:
WhatsApp tested side-by-side on the same iPhone/Watch pair does not show this drift.
I might actually have an explanation for this. I don't know if their app is doing this, but there's another way an NSE can get notifications to "the screen" beyond just posting modified content.
It's not necessarily well known, but an NSE can directly post local notifications (or delete), instead of just returning modified content. This is a very useful technique in messaging apps, as it lets you "backfill" missed messages and/or unrelated content. Putting that in more concrete terms, you can do things like this:
-
Your NSE receives a new notification.
-
Your NSE connects to the server, notices that it's missed 3 messages on the "current" thread and 2 messages on unrelated threads, and downloads all of the content for those other messages.
-
Your NSE posts 5 different local notifications for the missed notifications, "filling out" the content of those threads.
-
Your NSE then returns the content of the notification it actually received.
Any app can use the flow above; however, if your app has the filtering entitlement, then step #4 is optional, so you could just discard all of the "push" notifications and instead route everything through your local notification logic.
More to the point, there's a good argument that doing so is actually a BETTER architectural choice than going to the trouble of doing that "extra" push at the end. The issue here is that under real-world conditions, the range of possibilities quickly becomes fairly unwieldy and fairly weird.
For example, it's entirely possible that latency issues mean that the message data (embedded in the push) you're currently looking at is actually OLDER than the message data you end up getting from the server. You could "mask" that by shuffling the notifications you push such that the push you received is actually posted as a local notification and the most recent is sent as the NSE content; however:
-
That's a weird thing to try and logically think through in code.
-
It's entirely possible that you’ll shortly receive a new push for that most recent push, at which point you'll need to either discard THAT push or "fiddle" with notification content so you can repost the same notification again.
While it's certainly possible to make all of that work, it's actually somewhat easier to just filter "all" notifications and then have a "notification engine" which takes whatever data your NSE has/downloads and then posts everything it needs to through the local notification system. That way, there's only a single code path for every notification, and you don't have to worry about issues like the timing difference between your NSE returning data and local notification reporting.
In any case, it’s entirely possible that they’re doing what I’ve described above, which might explain the difference in behavior.
__
Kevin Elliott
DTS Engineer, CoreOS/Hardware