Keeping a Mesh Running in the iOS Background
A real-time P2P mesh on iOS has to work within strict background limits, the memory watchdog and the 0xdead10cc termination for holding a database lock. We close the database before suspension and use Bluetooth state restoration.
A peer-to-peer mesh messenger has to stay connected in the background, and iOS does not make that easy. iOS strictly limits persistent background processes to save battery life and keep the device responsive.
If you keep connections active, scan for Bluetooth peripherals, or write to a database in the background, iOS’s watchdog (RunningBoard) will kill your app. The exception code you are most likely to see is 0xdead10cc (decimal 3735883980). In this article, we explain how we work within the iOS background limits, how we fixed database locking crashes, and how we added persistent Bluetooth restoration for a cross-platform mesh.
1. What 0xdead10cc Means
When an iOS app moves to the background, the operating system gives it a grace period to clean up, after which it freezes the process (suspension). However, if your app holds a file lock or a SQLite database lock at the moment of suspension, the OS executes a SIGKILL with the exception code 0xdead10cc.
iOS does this so a suspended app cannot hold resources that other processes, such as App Extensions, need. VeilMesh is a local-first mesh app whose Rust core (via UniFFI) constantly interacts with a SQLite database, so avoiding this took sustained work.
2. Cooperative Database Lock Coordination
To prevent these crashes, we designed a Cooperative Database Lock Coordinator. When the app transitions to the background, the coordinator triggers a background task and calls lockDatabase(). On the Rust side, this sets an atomic is_locked flag, blocks the write actor loop, closes the read connection pool, and releases the database files.
We then found a race condition. When background tasks (like push notifications or BLE sync events) woke the app, they temporarily re-opened the database by incrementing an unlockCount. When their tasks finished, the count dropped back to 0 and the database was marked locked, but the SQLite file descriptors were never closed.
This left active locks on disk, resulting in a 0xdead10cc termination every time the app returned to suspension. We resolved this by modifying the lifecycle coordinator to explicitly close the SQLite database worker connections as soon as the reference count hits zero, freeing all file handles prior to suspension.
3. State Preservation and Restoration for Bluetooth
With the database crashes fixed, the next problem was keeping the mesh running. When an app is suspended or terminated by the OS due to memory pressure, all active Bluetooth scans and GATT connections are lost. To handle this, we implemented CoreBluetooth State Preservation and Restoration.
During the initialization of our central and peripheral managers, we register unique restoration identifiers:
CBCentralManagerOptionRestoreIdentifierKey: "com.veilmesh.ios.central"
CBPeripheralManagerOptionRestoreIdentifierKey: "com.veilmesh.ios.peripheral"
With state restoration on, iOS keeps scanning and watching our Bluetooth connections after the app is terminated. If a Bluetooth event occurs (such as a remote peer connecting or sending data), the kernel automatically re-launches our app in the background and invokes our restoration delegate methods. We then reconstruct our connection maps and resume packet routing.
4. Cross-Platform GATT Interoperability (Android & iOS)
In the background, Android and iOS only find each other if both follow the same Bluetooth rules. On iOS, background Bluetooth advertising is highly restricted: the local name is stripped, and the service UUID is moved to a private "overflow" area. Wildcard scanning cannot see these advertisements.
So that both apps find each other, they scan for one specific GATT service UUID. Since both Android and iOS scan explicitly for our designated service UUID, they can discover each other even when both apps are suspended. Android's active background scanning detects iOS's background advertisements, initiates a connection, and triggers the iOS background restore event, so devices on both platforms end up in the same mesh.
Conclusion
Closing the database before suspension stopped the 0xdead10cc terminations, and Bluetooth state restoration relaunches the app when a peer connects, so the mesh keeps working in the background.