Keeping a fleet of wearables synced through phones that want to sleep
A wearable that records continuously is only useful if the recording reaches a server. Between the device and the server sits a phone, and the phone’s operating system treats the companion app as a battery problem to be managed. We built the apps and the ingestion platform for Corsano Health and the data pipeline for CardioMood, and in both cases the sync path took more engineering time than the analytics on top of it. This is how that path works and where it breaks.
Pairing and the reconnect loop
Bluetooth Low Energy pairing is the easy part. The hard part is the reconnection the app has to perform many times a day without the user noticing. The phone walks out of range, the device drops the link, and both sides have to find each other again when they are back within a few metres.
On iOS the app asks CoreBluetooth to connect to a known peripheral and the request stays pending across app suspension; when the device advertises again, the system wakes the app in the background to handle the connection. On Android the equivalent is a connect call with autoConnect set, which is slower and less predictable, and which some manufacturers’ Bluetooth stacks handle badly. We keep a list of the phone models whose stacks need a workaround, and it is not short.
The reconnect loop needs backoff. A device that fails to connect ten times a minute drains both batteries and fills the logs with noise. A device that waits an hour between attempts loses data when the user is standing next to the phone. We use short retries for the first minute and longer ones after that, reset by any sign of the device advertising.
What the OS will and will not let you do
iOS gives a companion app a small set of background modes. The Bluetooth central mode lets the app wake for BLE events, process them and go back to sleep, usually within about ten seconds. Background app refresh is scheduled by the system and arrives when it decides, which in practice is a few times a day. Background URL sessions let a large upload continue after the app is suspended, and the system calls the app back when it finishes. An app that tries to stay awake by playing silent audio or asking for location it does not need will fail review or get killed.
Android is more permissive and less uniform. Doze mode defers network access and scheduled jobs when the phone sits unused, with maintenance windows that get rarer the longer it lies still. App Standby buckets give an app the user rarely opens fewer background opportunities. On top of the platform rules, several manufacturers add their own battery optimisation that kills background services outright unless the user finds a settings screen and excludes the app. A foreground service with a persistent notification is the reliable way to hold a BLE connection, and the user has to accept seeing that notification.
The design that survives accepts that the app will be asleep for hours at a stretch and makes that fine.
The device buffers, the phone catches up
Everything depends on the wearable holding its own data. A device with a day or more of local storage turns a phone that was away into a late upload rather than a hole in the record. When the link comes back, the app asks the device what it has since the last acknowledged record and pulls it down in order.
The transfer is slow relative to the data. BLE throughput in practice is tens of kilobytes per second, less on older phones, and a day of continuous signals from several sensors can take minutes to drain. The app has to survive being suspended halfway through that drain and resume from the last acknowledged record rather than from the start. The device must not delete a record until the phone has confirmed it, and the phone must not confirm until the record is in its own store.
Chunked, idempotent uploads
The phone uploads to the server in chunks, each one small enough to finish inside a background window. Every chunk carries the device identifier, the sequence range it covers and a content hash. The server treats a chunk it has seen before as a success and does nothing with it. A retry can therefore never duplicate data, so the app retries freely and the device resends freely, and everything else in the system leans on that property.
Duplicates are the failure we see most when this is missing. A phone uploads a chunk, loses the connection before the acknowledgement arrives, and uploads it again. Without idempotency the server now has two copies of six hours of heart rate, and the averaging downstream is wrong in a way nobody notices for weeks.
Ordering, gaps and clocks
Data arrives out of order. One phone uploads yesterday’s buffered records after today’s live ones. Two phones paired to one device, which happens when someone changes handset, upload overlapping ranges. The server has to order by the device’s own sequence numbers and timestamps, and store a gap as a gap when the sequence skips.
Clocks drift. The wearable’s clock is set from the phone at connection time and wanders between connections, by seconds a day on a cheap oscillator. If the device was away for a week, its timestamps can be off by a minute by the time it reconnects. We record the phone’s time and the device’s time at every sync and store the offset with the records, so the correction can be applied later and revisited if it was wrong. Stamping records with the server’s arrival time would be simpler, and would be wrong for everything that was buffered.
Watching the fleet
A single device going quiet is a support ticket. A thousand devices going quiet on the same day is an OS update, and we want to know within hours. The server keeps, per device, the time of the last record received and the timestamp inside that record, and alerts when the fleet’s distribution shifts. A rise in devices that have not synced for more than a day, broken down by phone platform and OS version, points straight at the cause.
The same breakdown catches a bad firmware release, a manufacturer’s new battery policy and a Bluetooth stack regression in a phone update. We check it the morning after every major iOS and Android release.
What the user sees
Show the user the last time the device synced, in plain words, and what to do if it is old. “Last synced 20 minutes ago” is reassuring. “Last synced 3 days ago, open the app with the device nearby” gives a person an action. On Android, when the app detects that a manufacturer’s battery optimisation is on, it should say so and link to the settings screen, because nothing else will fix it.
Never show a chart that quietly interpolates across missing hours. A coach or a clinician who sees a smooth line through a period when the device was in a drawer will stop trusting every line after it.
If you are building on a wearable and the sync path is where the time is going, the wearables and health tech page covers what we build, and you can write to us about the device you have.