
BLE Reconnection Without Duplicating a Week of Data

Reconnecting a BLE peripheral after a drop is straightforward. Reconnecting without replaying seven days of sensor readings — and without losing any of them — is where most implementations quietly break.
What Actually Happens During a BLE Disconnection
BLE connections drop. Interference, range, sleep cycles, phone screen-off events — any of these can sever a link. The Bluetooth 5.x spec gives you a supervision timeout (configurable, typically 4–20 seconds on most peripheral stacks) after which both sides declare the connection gone.
From that moment, the peripheral has three choices:
- Keep buffering data into local storage (flash or SRAM) until reconnection
- Drop data and restart a clean session on reconnect
- Enter a low-power advertisement loop and wait
Most firmware teams go with option 2 because it is the simplest. The central (usually a mobile app or a gateway) reconnects, the peripheral says "fresh start," and whatever accumulated in the gap is gone. For heart-rate monitors or step counters, that is acceptable. For industrial sensors, cold-chain loggers, or any device with a regulatory audit trail, it is not.
The buffer problem
Buffering sounds trivial until you calculate the numbers. A sensor sampling at 10 Hz writing 20 bytes per sample generates 1.2 MB per day. Seven days is 8.4 MB. Most Nordic nRF52840 designs give you 1 MB of flash after the SoftDevice and application eat their share. You are already over budget before the first week ends.
External SPI flash solves capacity, but it introduces wear levelling, pointer management, and the question of what "oldest unacknowledged record" means when the central crashed mid-transfer.
How Do You Know What the Central Already Has?
This is the core engineering problem. When the peripheral reconnects, it needs to know exactly where the central's last confirmed read ended.
The naive solution is a sequence number. The peripheral assigns a monotonically incrementing uint32 to every record. On reconnect, the central tells the peripheral "my last confirmed record was 48201" and the peripheral begins streaming from 48202 onward.
That works until the central's own state is lost — app reinstall, phone wipe, backend database failure. Now the central claims it has seen nothing, and the peripheral has already overwritten records 1 through 30000 due to ring-buffer rotation. You have a gap you cannot fill.
A more durable approach uses timestamps as the primary key rather than sequence numbers, combined with a two-phase acknowledgement:
- Central sends a
SYNC_REQUESTwith its last confirmed UTC timestamp (or 0 if it has no state) - Peripheral responds with the earliest record it still holds that is newer than that timestamp
- Transfer proceeds in chunks; central ACKs each chunk with the timestamp of the last record it has committed to persistent storage
- Peripheral advances its "safe to overwrite" pointer only after receiving that ACK
The timestamp has to come from somewhere reliable. GPS-disciplined peripherals have it easy. BLE-only devices typically receive a current time from the central on each connection using the Current Time Service (CTS, GATT service 0x1805). Store that offset relative to the peripheral's internal RTC; do not trust the RTC alone across battery pulls.
Handling clock drift
A typical 32.768 kHz crystal oscillator drifts ±20 ppm. Over seven days, that is ±12 seconds. That is fine for most use cases. If you are correlating readings across multiple peripherals, you need to either re-sync the clock on every connection or store the peripheral's raw tick count alongside the UTC timestamp so you can post-correct it on the backend.
/// Not sure where to start?
Get the architecture before you commit
Tell us what you're building and we'll map the technical approach, stack, and rough timeline. No cost, no obligation, no sales call required.
What Does the GATT Transfer Actually Look Like?
A custom GATT profile for this typically has four characteristics:
| Characteristic | UUID suffix | Properties | Purpose |
|---|---|---|---|
| SYNC_REQUEST | 0x0001 | Write | Central sends its last confirmed timestamp |
| SYNC_RESPONSE | 0x0002 | Notify | Peripheral sends oldest available record timestamp |
| DATA_STREAM | 0x0003 | Notify | Chunked records, each prefixed with sequence num + timestamp |
| ACK | 0x0004 | Write | Central confirms last committed timestamp |
BLE 5.x with a 251-byte ATT MTU (negotiated via ATT_EXCHANGE_MTU_REQ) lets you fit roughly 244 bytes of payload per packet after the ATT and L2CAP headers. At 10 Hz with 20-byte records, you can batch 12 records per packet. With a connection interval of 7.5 ms and 0 peripheral latency, throughput is high enough to replay a week of data in minutes rather than hours.
Packet loss within a BLE connection is handled by the Link Layer retransmission mechanism — you do not need to implement your own ARQ inside GATT. What you do need is the ACK characteristic, because the Link Layer only guarantees delivery to the peer's radio buffer, not to your application's persistent storage.
iOS and Android caveats
iOS CoreBluetooth does not expose the ATT MTU negotiation API directly. You call maximumWriteValueLength(for: .withoutResponse) and use whatever the system negotiated. On most current iPhones with BLE 5.0 peripherals this lands at 185 bytes. Android's BluetoothGatt.requestMtu(517) call gives you more control, though 517 is the BLE 5.x maximum and few peripherals actually support it; 251 is a safer target.
Background execution is the bigger constraint. iOS will suspend your app after roughly 30 seconds of background BLE activity unless you declare bluetooth-central in UIBackgroundModes. Even then, you get limited CPU time. Design the transfer to be restartable: if the app is suspended mid-transfer, the next connection picks up from the last ACK'd timestamp.
Why Ring Buffers Beat Append-Only Logs Here
An append-only log is conceptually cleaner. You never overwrite anything, you just grow. In practice, on a device with 8 MB of external flash and a seven-day retention requirement, you either size the flash to fit worst-case data rates exactly or you add complexity to handle the "flash is full but we haven't been connected in 10 days" case.
A ring buffer with a write pointer and a "safe to overwrite" pointer (updated on ACK) gives you predictable memory behaviour. The trade-off: if the central is offline for longer than your buffer depth, you lose the oldest data. For most applications, losing day 8 when you have a seven-day SLA is acceptable. If it is not acceptable for yours, you need either more flash or a secondary uplink (LoRaWAN, LTE-M) to drain the buffer independently of BLE.
The ring buffer also makes it easy to calculate remaining capacity and surface that to the user: (write_ptr - safe_ptr) * record_size gives you bytes in use. Expose that via a GATT characteristic and the central can warn the user before data is at risk.
What Breaks in Production That Didn't Break in the Lab
Power cycling during an ACK write is the most common silent failure. The central writes the ACK, the peripheral's flash write is interrupted, and on the next connection the safe-to-overwrite pointer is behind where it should be. You replay some records you have already stored. That is better than losing data, but it means your deduplication logic on the backend must key on the timestamp+sequence pair, not on arrival order.
The second failure mode is timezone and DST changes. If the user travels or their phone auto-updates to daylight saving time between connections, the timestamp the central sends in SYNC_REQUEST may be discontinuous with what the peripheral has stored. Storing everything in UTC on both sides removes this entirely. Never store local time on a BLE peripheral.
The third is BLE address rotation. iOS rotates its Bluetooth address periodically. If your peripheral bonds to a specific central address, it will refuse reconnection from a rotated address. Use the Identity Resolving Key (IRK) from the bonding process rather than the static address to identify the central.
Conclusion
The reconnection itself is a few lines of SoftDevice API calls. The hard part is the state machine around what has been confirmed, what is safe to discard, and what the central actually needs. Get the timestamp sync, the ACK loop, and the ring-buffer pointer logic right in firmware, and a week of offline data survives reconnection cleanly.
If you are at the point of specifying a GATT profile for a production device and want a second opinion on the buffer sizing or the sync protocol, talk to the team at Sodio. We have built this end-to-end across firmware, mobile, and backend and can usually spot the edge cases before they reach the field.
FAQ
Does BLE 5.x solve the reconnection data-loss problem on its own? No. BLE 5.x improves throughput and range, but the spec says nothing about application-layer state between connections. The peripheral firmware and the central app must implement their own sync protocol. The standard GATT services do not include a history-replay mechanism.
How long does it take to replay a week of sensor data over BLE? It depends on connection interval, ATT MTU, and record size. With a 7.5 ms connection interval, 244-byte payload, and 20-byte records, you can transfer roughly 120 KB/s of application data in ideal conditions. Seven days at 10 Hz and 20 bytes per record is about 120 MB — so replay takes roughly 16 minutes under ideal RF conditions, longer in practice.
Can I use the standard BLE Current Time Service for clock sync? Yes, CTS (GATT 0x1805) is the right tool. Write the current UTC epoch to the peripheral on each connection and store the delta between that and the peripheral's RTC tick. Do not rely on the RTC alone across power cycles; battery-backed RTCs drift and lose state on deep power-down.
What happens if the central's database is wiped and it requests a full resync? The peripheral can only give back what it still has in its ring buffer. If the retention window is seven days and the central was wiped eight days ago, the oldest three days are gone. There is no protocol-level solution; this is a product decision about acceptable data loss, which should be reflected in your retention SLA and buffer size.
Should I implement deduplication on the peripheral or the backend?
The backend. The peripheral should send everything it has from the requested timestamp forward. Deduplication logic on constrained firmware is a maintenance burden and adds failure modes. A backend dedup keyed on device_id + sequence_number is trivial and much easier to fix if it has a bug.
Have a project in mind? Contact Sodio Technologies to discuss your requirements and explore the right technology solution for your business.
/// Work with us
Talk to the engineers who'd build it
You'll get a technical scope, timeline and cost estimate from the people doing the work, not an account manager. In-house team, no subcontracting, since 2016.
