How to Handle BLE Reconnection After iOS Background Termination
Your EEG companion app works flawlessly in the foreground. The user locks their phone, walks around for twenty minutes, and when they come back the app has lost its connection to the headset. Worse, it has no idea the headset is still advertising. This is one of the most common and frustrating problems in BLE-based neurotech products on iOS, and fixing it properly requires understanding exactly how CoreBluetooth behaves when your app is suspended or terminated by the system.
We have spent years building BLE reconnection logic for EEG headsets at DEVSFLOW, including work on the RE-AK Nucleus-Kit and CLEIO devices. This post covers the specific mechanisms iOS provides for background BLE reconnection, the edge cases that will bite you in production, and the strategies that actually work.
How iOS Background Execution Affects BLE
iOS manages app lifecycle aggressively. When your app moves to the background, it gets roughly 30 seconds of continued execution before being suspended. Once suspended, your app's code does not run at all. If the system needs memory, it will terminate your app entirely, removing it from RAM without calling any cleanup methods.
CoreBluetooth has a special relationship with background execution. If your app declares the bluetooth-central background mode in its Info.plist, it can continue scanning and maintaining connections while suspended. But "can" is doing a lot of heavy lifting in that sentence. The reality is more nuanced.
In the background, scanning behavior changes fundamentally. The CBCentralManagerScanOptionAllowDuplicatesKey option is ignored, meaning you receive only one discovery event per peripheral per scan session. Scan intervals are stretched to save power. If the system terminates your app, all active BLE connections are dropped and all pending connect() calls are cancelled.
State Restoration: The Core Mechanism
CoreBluetooth state restoration is Apple's solution for reconnecting after termination. When you initialize your CBCentralManager with a restoration identifier, the system preserves your Bluetooth state even after your app is killed. If a peripheral you were connected to (or trying to connect to) becomes available again, iOS relaunches your app in the background and calls centralManager(_:willRestoreState:).
Setting this up requires two things:
- Pass a unique string as
CBCentralManagerOptionRestoreIdentifierKeywhen creating yourCBCentralManager. This identifier must be consistent across app launches. If you use a different string each time, state restoration will never fire. - Implement
centralManager(_:willRestoreState:)in your delegate. This method receives a dictionary containing the peripherals that were connected or pending connection at the time of termination, any active scan services, and the scan options that were in use.
The dictionary keys you care about are CBCentralManagerRestoredStatePeripheralsKey (an array of CBPeripheral objects) and CBCentralManagerRestoredStateScanServicesKey (an array of CBUUID objects). The peripherals in this array are already in a connected or connecting state. You need to set their delegates immediately and resume your data pipeline.
Peripheral Identifier Caching
Every CBPeripheral has a identifier property, which is a UUID that iOS assigns and keeps stable for a given peripheral on a given iOS device. This is not the peripheral's MAC address. It is a local identifier that persists across app launches and even across Bluetooth resets, as long as the peripheral's identity resolving key (IRK) has not changed.
You should cache this identifier in UserDefaults or your local database immediately upon first successful connection. When your app launches (or relaunches via state restoration), use centralManager.retrievePeripherals(withIdentifiers:) to get a reference to the peripheral object without scanning. This is significantly faster than a full scan and works even when the peripheral is not currently advertising.
If retrievePeripherals returns your device, call connect() on it. CoreBluetooth will queue this connection request and fulfill it whenever the peripheral becomes available, even if that is minutes or hours later. This pending connection survives app suspension but not app termination, which is why state restoration exists.
The Reconnection Strategy That Works
After extensive testing with EEG headsets, we use a layered reconnection approach:
- On
centralManagerDidUpdateStatewith.poweredOn, immediately callretrievePeripherals(withIdentifiers:)with cached identifiers. If a peripheral is returned, callconnect(). - If
retrievePeripheralsreturns nothing, callretrieveConnectedPeripherals(withServices:)using your EEG service UUID. This catches cases where another app or the system has an active connection to the headset. - If neither retrieval method works, start a filtered scan for your service UUID. In the background, this scan is throttled but still functional.
- If state restoration fires via
willRestoreState, skip steps 1 through 3. The peripherals are already provided. Set delegates and proceed.
Each of these steps feeds into the same connection pipeline. Once didConnect fires, discover services, discover characteristics, enable notifications, and resume your data stream. The entire pipeline must be idempotent because you cannot predict which entry point will fire.
Edge Cases with EEG Headsets
Consumer EEG headsets introduce several complications that generic BLE reconnection guides do not cover:
- Many headsets (including devices based on the NeuroSky TGAM and some OpenBCI variants) do not use bonding. Without a bond, the peripheral's identity can change if it rotates its BLE address. If your cached UUID suddenly stops matching, you need to fall back to service-based scanning.
- Some headsets enter a "pairing mode" after losing connection, advertising with different service UUIDs or advertising data than during normal operation. Your scan filter must account for both states.
- EEG headsets that use BLE 4.2 without LE Secure Connections will have their MAC address rotated by iOS's privacy features. The
CBPeripheral.identifierstill remains stable for a given peripheral, but only if the peripheral consistently resolves to the same identity. If the headset's firmware updates its BLE stack or changes its advertising address scheme, you may lose the identifier mapping. - After reconnection, the headset's internal state may have changed. It might have continued sampling and filled its internal buffer, or it might have entered a low-power state that requires a specific initialization sequence. Your app must send a status query command immediately after reconnection and adapt accordingly.
Testing Background Termination
You cannot reliably test state restoration by force-quitting the app from the app switcher. Force-quitting via the app switcher explicitly disables state restoration and background relaunch. The user has expressed intent to kill the app, and iOS respects that.
To test system-initiated termination, you need to simulate memory pressure. The most reliable method is to open many memory-intensive apps while your BLE app is in the background, forcing iOS to terminate it. Alternatively, you can use Xcode's Debug menu to simulate a memory warning, though this does not always trigger actual termination.
We maintain a test harness that logs every state transition: suspension, termination, restoration, and reconnection timestamps. After each build, we run a test cycle where the app connects to a headset, moves to background, gets terminated via memory pressure, and must reconnect within 10 seconds of the headset becoming available again. Any build that fails this cycle does not ship.
Common Mistakes
Three mistakes account for most of the BLE reconnection bugs we see in neurotech apps:
- Creating a new
CBCentralManagerinwillRestoreState. The manager that called your delegate is already restored. Creating a second one creates a conflicting Bluetooth session and leads to undefined behavior. - Not handling the case where
willRestoreStateprovides peripherals in a.disconnectedstate. If the peripheral disconnected while your app was terminated, you will still get a restoration event, but the peripheral will not be connected. You must callconnect()again. - Assuming state restoration will always fire. If the user force-quits your app, if iOS performed a Bluetooth reset, or if more than a few hours have passed since termination, state restoration may not trigger. Your normal connection flow must work independently of restoration.
BLE reconnection on iOS is one of the trickiest parts of building a reliable neurotech companion app. If your team is struggling with background BLE behavior for EEG or biometric devices, talk to DEVSFLOW Neuro. We build mobile apps that maintain rock-solid connections to headsets and wearables.