TelephonyMessagingKit drops first SMS at cold launch — race between client XPC handler registration and server pending flush

Hi all,

I'm the developer of OV Message, an end-to-end encrypted SMS messaging app already shipped on Google Play (Android, where it natively encrypts SMS content). The iOS port aims to be the default carrier-messaging app, handling SMS, MMS, and RCS through TelephonyMessagingKit with the com.apple.developer.carrier-messaging-app entitlement under the EU programme. While testing the cold-launch flow on iOS 26.x, I've hit a reproducible bug that silently drops the first SMS/MMS/RCS that wakes the app, and I'd like to confirm whether other devs working with this API see the same.

The bug

When a default carrier-messaging app is force-killed and a message arrives, iOS correctly:

  1. Routes the message via CommCenter (IMS in my case — SFR France)
  2. Wakes the app in background (state = .background at didFinishLaunchingWithOptions)
  3. Acquires a TelephonyMessaging runningboard assertion on the app

But CommCenter then pushes the pending message via XPC before the client TMK library has finished registering its messageHandlersByID dictionary. Result: client responds Received unhandled request, server logs TMKXPCError Code=2, message is dropped, never delivered to for await in incomingMessageNotifications. Subsequent messages (with the app warm) work fine.

Native log sequence (from idevicesyslog with the Telephony logging profile)

T+0.000  CommCenter: SMS arrives via IMS (k3GPP)
T+0.003  CommCenter: Default app is set to com.example.app
T+0.004  CommCenter: Attempting to launch and acquire process assertion
T+0.083  CommCenter: Notifying SMS message received, target: bundleID=...
T+0.085  CommCenter(TMK): There are no client connections matching, pending message
[~125 ms — app boots]
T+0.128  App(TMK): Configuring connection
T+0.128  App(TMK): Pinging remote end
T+0.130  CommCenter(TMK): Received new connection from PID
T+0.130  CommCenter(TMK): New incoming connection, flushing pending messages (1)   ← server flushes
T+0.130  App(TMK): Received unhandled request                                       ← client not ready
T+0.131  CommCenter(TMK): Failed to send pending message: TMKXPCError Code=2
T+0.132  App(TMK): Registered for IncomingMessageNotification (smsReceived)         ← ~2 ms too late

The race window between Pinging remote end (client) and Registered for IncomingMessageNotification (client) is 2–7 ms across my measurements. CommCenter considers the connection ready as soon as the ping completes, but the client library populates messageHandlersByID slightly after, so the dispatch fails.

Minimal reproduction

I built a ~50-line Swift app to confirm this isn't specific to OV Message. UIKit AppDelegate, single for await in TelephonyMessagingSession.shared.smsService.incomingMessageNotifications started in didFinishLaunchingWithOptions. No SwiftUI, no other modules, no Darwin notifications. Just TMK.

Steps:

  1. Build & install on iPhone iOS 26.x with carrier-messaging-app entitlement (auto-provisioned in iOS 26)
  2. Settings → Apps → Default Messaging → select the test app
  3. Force-kill, then send 2 SMS in rapid succession from another phone
  4. Wait 30 s, open the app — log shows only the 2nd SMS

Same result: the 1st SMS is gone. I've reproduced this consistently dozens of times.

Source code (Swift + xcodegen project.yml): https://gist.github.com/ovmessage/fbc529292a65222191bec6ce5e5a4275

What I've tried

  • Task.detached(priority: .userInitiated) to decouple the for await from main thread scheduling — no effect (race is internal to TMK lib, before our scheduling)
  • Pre-fetching cellularServices synchronously — no effect
  • Subscribing MMS + RCS in parallel — no effect
  • Direct XPCSession/xpc_connection_create_mach_service to com.apple.commcenter.tmk.xpc — Apple has marked these unavailable on iOS for 3rd-party apps (no public way to bypass the lib)

I've also done runtime introspection of the TMK framework via Mirror, which confirms the architecture: a single XPCConnection.messageHandlersByID dict shared by smsReceived, mmsReceived, rcsReceivedNotification — all four entries (incl. serviceStatusNotification) are populated after the XPC ping. So the same race affects SMS, MMS, and RCS equally.

Suggested fixes (Apple-side)

Either:

  1. Server (CommCenter): defer flushing pending messages until the client confirms its handlers are registered (extra XPC handshake message)
  2. Client (TelephonyMessagingKit): register messageHandlersByID entries before sending Pinging remote end, so they exist when the server starts flushing
  3. Buffer client-side: cache messages received before handler registration completes, dispatch on attach

Filed in Feedback Assistant

FB[YOUR_FB_NUMBER_HERE]

Question for fellow devs

If you're also building with carrier-messaging-app entitlement (Beeper, Google Messages on iOS, anyone in the EU programme), can you confirm whether you see the same race? Especially interested in whether:

  • It happens with non-IMS carriers (mine is SFR France, IMS-routed via SIP)
  • iOS 26.1 / 26.2 changed the timing
  • Anyone has found a workaround I haven't tried

Thanks.

The bug When a default carrier-messaging app is force-killed and a message arrives, iOS correctly:

If you haven't already, please file a bug on this and then post the bug number back here.

Anyone has found a workaround I haven't tried?

Maybe. Try calling "isConfiguredForCarrierMessaging" before you "touch" any of the service objects. If that still fails, then I'd use a task to briefly delay session creation a while after you called isConfiguredForCarrierMessaging.

There's actually infrastructure in place that's supposed to handle this issue. I'm not sure why it's failing, but my best guess is that this particular sequence means that the pending queue isn't set up properly. Calling isConfiguredForCarrierMessaging might give that infrastructure the head start it needs to avoid this issue.

__
Kevin Elliott
DTS Engineer, CoreOS/Hardware

Hi Kevin,

Thanks a lot for jumping on this — really appreciate it.

I tested the pattern you suggested in a fresh, isolated repro: read isConfiguredForCarrierMessaging first, then delay session creation via a Task.sleep before touching smsService.incomingMessageNotifications. I varied the delay across four values to be thorough.

Code shape (full source available, Swift, ~50 lines):

let session = TelephonyMessagingSession.shared
let configured = session.isConfiguredForCarrierMessaging          // PHASE 1
TMKLogger.shared.log("isConfiguredForCarrierMessaging=\(configured)")

Task.detached(priority: .userInitiated) {
    try? await Task.sleep(nanoseconds: kDelayMs * 1_000_000)      // PHASE 2
    let notifications = try session.smsService.incomingMessageNotifications  // PHASE 3
    for await notification in notifications {
        TMKLogger.shared.log("✅ SMS received: \(notification.message.content.body)")
    }
}

Test protocol on each delay:

  1. Settings → Apps → Default Messaging → TMKTest
  2. Force-kill the app
  3. Send two SMS in rapid succession from another phone
  4. Wait 30 s without touching the phone
  5. Open TMKTest, read the persistent log

iPhone 14 Plus, iOS 26.4.2, SFR France (IMS-routed SIP).

Empirical results

Delay1st SMS2nd SMSPhase 3 ready at
200 ms❌ lost✅ receivedT+0.2 s
500 ms❌ lost✅ receivedT+0.5 s
1000 ms❌ lost✅ receivedT+1.0 s
2000 ms❌ lost✅ receivedT+2.0 s

The 1st SMS is dropped identically across all four runs. The 2nd SMS arrives normally. Pattern is rigorously identical regardless of delay length.

A relevant detail

In every test, isConfiguredForCarrierMessaging returns true synchronously before the delay starts (1–19 ms after didFinishLaunchingWithOptions, never failing). If reading that flag is what arms the pending-queue infrastructure you mentioned, that infrastructure should already be in place by the time the delay runs and Phase 3 fires. Yet the message is still flushed to a connection that doesn't have its handlers registered yet — the native CommCenter logs continue to show TMKXPCError Code=2 and Received unhandled request regardless of how long we wait before touching the service.

This suggests the race isn't between "infrastructure armed" and "service touched", but rather inside the XPC handshake itself: messageHandlersByID on the client XPCConnection is populated after Pinging remote end returns, and CommCenter starts flushing pending messages on Received new connection from PID — that ~2–7 ms gap is what we keep losing the 1st message in. Delaying anything before smsService access doesn't move the XPC handshake — it just delays it later.

Question

In the runtime introspection I did earlier on the framework binary, I found a TelephonyMessagingKit.Messaging.Server type (mangled _$s21TelephonyMessagingKit0B0O6ServerC) with what appears to be a synchronous callback method setIncomingMessageHandler<A: Message>(@Sendable (XPCPeerMessage<A>) throws -> ()). A synchronous handler installed at session creation would sidestep the for await race entirely.

Is that an internal-only API for now, or is there any chance it's being considered for @_spi exposure to carrier-messaging-entitled apps in a future iOS 26.x point release? If not, would you have any other client-side angle to try, or is the right path here to wait for a server-side fix in CommCenter (option 1 in my original post: defer flushing until the client confirms handler registration)?

Thanks again for taking the time on this — happy to share the full TMKTest_Workaround repro project (xcodegen + 4 Swift files) if useful.

Best,

TelephonyMessagingKit drops first SMS at cold launch — race between client XPC handler registration and server pending flush
 
 
Q