Store-and-Forward BLE: How to Buffer Data When Your Gateway Goes Offline

Your BLE sensor node samples temperature every second. The gateway reboots for a firmware update. Ninety seconds later, it’s back online. Those 90 readings? Gone. GATT notifications are fire-and-forget, and your firmware had nowhere to put them.
This is the most common data loss scenario in BLE deployments, and it’s entirely preventable. The fix is a firmware architecture pattern called store-and-forward: buffer data locally on the peripheral, detect when the connection comes back, and drain the buffer in order. BLE gives you zero help here at the protocol level. You have to build it yourself.
This article walks through the full pattern: a two-tier buffer architecture (RAM plus flash), disconnection handling and reconnection strategy, and a sequenced drain protocol that prevents duplicates and gaps. The goal is zero data loss during gateway outages, whether they last 2 seconds or 2 hours.
Why BLE Won’t Do This for You
If you’re coming from MQTT (with QoS 1 or 2) or LoRaWAN (with its confirmed uplink mechanism), you might expect some built-in delivery guarantee. BLE doesn’t have one.
GATT notifications carry no application-level acknowledgment. You push a value, the stack sends it, and you get no confirmation that the other side actually received and processed it. GATT indications are slightly better: they include a link-layer ACK. But that ACK only means “the BLE stack on the remote device received the PDU.” It says nothing about whether the application layer consumed it, stored it, or choked on it.
There’s no message queue, no retry mechanism, no session resumption. On BLE, your firmware is the message broker. You need to design the queuing, the delivery tracking, and the retransmission logic from scratch.
Architecture Overview: The Store-and-Forward Pipeline
Think of the whole system as a state machine with 5 stages: Sample → Buffer → Detect Connection → Drain → Confirm.
┌──────────┐ ┌────────────┐ ┌────────────┐ ┌──────────┐
│ Sensor │───▶│ RAM Ring │───▶│ BLE TX │───▶│ Gateway │
│ Sample │ │ Buffer │ │ (drain) │ │ (ACK) │
└──────────┘ └─────┬──────┘ └────────────┘ └──────────┘
│ overflow ▲
▼ │
┌────────────┐ │
│ Flash/NVS │──────────────────────────┘
│ Overflow │ (drain after RAM empty)
└────────────┘The two-tier buffer is the core idea. RAM is the hot tier: fast, cheap to write, but volatile and limited. Flash (or NVS) is the cold tier: persistent across power cycles, larger, but expensive in terms of write energy and wear.
During normal operation, sensor readings land in a RAM ring buffer and drain directly over BLE to the gateway. When the connection drops, the RAM buffer starts filling. If the outage lasts long enough to hit a high-water mark, records spill to flash. On reconnect, flash drains first (oldest data), then RAM resumes live drain. This preserves FIFO ordering across both tiers.
The pattern works for any intermittent wireless link; BLE’s MTU size and connection intervals just shape the implementation details.
┌─────────────────┐
│ CONNECTED │
│ (live drain) │◀──────────────────┐
└───────┬──────────┘ │
│ disconnect event │ connection
▼ │ established
┌─────────────────┐ │
│ DISCONNECTED │ │
│ (buffer to RAM) │───────────────────▶│
└───────┬──────────┘ adv + reconnect
│ RAM high-water
▼
┌─────────────────┐
│ OVERFLOW │
│ (spill to flash)│────────────────────┘
└──────────────────┘ adv + reconnectDesigning the RAM Ring Buffer
A ring buffer (circular buffer) gives you O(1) enqueue and dequeue, fixed memory footprint, and zero fragmentation. It’s the right default for the hot tier.
Size it based on your sample rate and the outage duration you want to handle purely in RAM: buffer_entries = sample_rate_hz × max_ram_outage_seconds. At 1 sample/sec with 20-byte records and a target of 60 seconds of RAM buffering, you need 1,200 bytes, manageable on almost any modern BLE SoC.
Each record should be a fixed-size struct:
typedef struct {
uint32_t sequence; // monotonic counter
uint32_t timestamp; // ms since epoch or uptime
uint8_t payload[12]; // sensor data
} record_t; // 20 bytes, no padding surprises
The sequence number is how you and the gateway track what’s been delivered and what hasn’t. Don’t skip it. Without it, you have no way to resume a partial drain after a second disconnection.
Overflow policy matters. If you’re building a consumer fitness tracker, overwriting old data might be fine. If you’re monitoring industrial equipment or tracking assets, you can’t afford to lose readings. Spill to flash instead of overwriting.
For thread safety, if you’re sampling from an ISR and draining from a BLE task, you have two options: a lock-free single-producer single-consumer ring buffer (volatile head and tail pointers), or an RTOS message queue. The lock-free approach is simpler and doesn’t block your ISR.
Flash Overflow: Persisting Data Through Power Cycles
When the RAM ring buffer crosses your high-water mark (75% to 80% full), start spilling to flash. Also spill if the device is about to enter deep sleep or reset.
You have 3 storage options, each with trade-offs:
- Raw flash sectors with manual wear leveling. Maximum control, minimum overhead, but you own every detail of erase management.
- NVS or key-value store (like Zephyr’s NVS subsystem). Handles wear leveling for you. Slightly more overhead per record.
- LittleFS or similar power-loss-resilient filesystem. Most flexible, but the largest RAM and code footprint.
For store-and-forward buffering, raw sectors or NVS tend to be the right fit. A full filesystem is overkill unless you’re also storing config, logs, or other data.
Some practical constraints to keep in mind. At 1 sample/sec with 20-byte records, a 4KB flash sector holds roughly 200 records, about 3.3 minutes of data. If your worst-case outage is 2 hours, you need about 36 sectors (144KB). That’s feasible on most BLE SoCs, but plan for it early.
Wear leveling is essential. Rotate writes across all available sectors and track erase counts. Most NOR flash supports 100K erase cycles per sector, which sounds like a lot until you do the math on a device that loses connectivity daily. More on that in the constraints section below.
Power-loss safety is the hard part. Never trust a write that wasn’t completed. Use flash’s natural 0xFF-to-0x00 bit transitions as state markers. Specifically, write a validity byte as the last step of committing a record. If the write was interrupted, the validity byte stays 0xFF, and you know the record is incomplete. On boot, scan for records with valid markers and discard anything else. Add a CRC to each record for corruption detection.
Only erase a sector after every record in it has been ACKed by the gateway.
BLE Disconnection Handling and Reconnection
Your BLE stack will fire a disconnection event (BLE_GAP_EVT_DISCONNECTED on Nordic’s SoftDevice, bt_conn_cb.disconnected on Zephyr, or equivalent). This is your trigger to flip the state machine from CONNECTED to DISCONNECTED and start buffering.
Supervision timeout tuning is a balancing act. A shorter timeout (1 to 2 seconds) means faster detection of a dead link. A longer timeout (4+ seconds) tolerates transient RF interference without triggering a false disconnect. For most sensor deployments, 2 to 4 seconds is a reasonable starting point. If you’re working with the Hubble terrestrial network, check the transmission guidance for specific recommendations.
For reconnection, peripheral-initiated advertising is usually more power-efficient than waiting for the central to scan and reconnect. Use directed advertising toward the gateway’s address for the first few attempts, then fall back to undirected.
Exponential backoff on advertising intervals saves significant power during extended outages. Start at 20ms intervals for quick reconnection, then widen to 1s, then 5s, then 30s. There’s no point burning milliamps advertising every 20ms when the gateway has been gone for 10 minutes.
On reconnect, request a shorter connection interval for the drain phase (7.5ms to 15ms). Once the buffer is empty, negotiate back to a relaxed interval (500ms+) to save power during normal operation.
The Drain Protocol: Sequenced Retransmission After Reconnect
When the connection comes back up, push buffered data to the gateway in FIFO order, oldest first. Set up two custom GATT characteristics: one for data (notify), one for control (write). The peripheral sends records on the data characteristic. The gateway writes the last confirmed sequence number to the control characteristic.
Chunking depends on your negotiated ATT_MTU. Modern stacks commonly negotiate 247 bytes. With 20-byte records, you can pack up to 11 records into a single notification (with a 2-byte header for count). Fewer notifications means fewer connection events means less radio-on time.
Flow control is critical. If you blast notifications as fast as the stack will accept them, you’ll overflow the BLE stack’s internal TX buffer and start dropping packets, defeating the entire purpose. Use connection event callbacks or a credit-based scheme: send N notifications, wait for the stack’s TX-complete callback, send N more.
The gateway ACK mechanism has two reasonable options:
Option A: Gateway periodically writes the last confirmed sequence number to the control characteristic. The peripheral marks everything up to that sequence as safe to delete. Efficient, batched.
Option B: Use GATT indications instead of notifications for each chunk. The ATT-level ACK confirms receipt. Simpler to implement, but slower because you can only have one indication in flight at a time.
I’d recommend Option A for any deployment where drain speed matters.
The sequence number handles the ugly edge case: reconnect, partial drain, disconnect again. Both sides know exactly where they left off. The gateway should be idempotent, able to handle receiving the same sequence number twice without creating duplicate records.
If you’re building a cloud integration behind the gateway, Hubble’s webhook-based data retrieval can handle the backhaul side of this, including deduplication.
Managing the Three Constraints
Every design decision in store-and-forward boils down to three competing constraints:
Constraint Impact Mitigation
───────────────────────────────────────────────────────────────────
Flash wear Sector death over time Sector rotation, write batching
Power budget Reduced battery life Batch flash writes, backoff adv
BLE MTU size Slow drain, more radio time Negotiate max MTU, pack records
Connection interval Drain throughput ceiling Request fast CI during drainFlash wear deserves careful calculation. Say your device loses connectivity for 30 minutes daily and writes 20-byte records at 1Hz. That’s 1,800 records per day. A 4KB sector holds about 200 records, so you’re erasing 9 sectors daily. Spread across 16 sectors with rotation, each sector gets erased roughly every 2 days. Over a year, that’s about 180K erases per sector, well past the typical 100K rating. You’d need more sectors or larger write batching to bring that number down.
Power is more intuitive. Flash writes on most NOR flash draw 10 to 15 mA for 1 to 5 ms per page. Batch your writes to amortize the startup cost. And make sure your advertising backoff is aggressive enough that you’re not wasting milliamp-hours calling out to a gateway that isn’t there.
MTU is the easiest to optimize. Negotiate the largest MTU your stack supports at connection time. Going from the default 23-byte MTU to 247 bytes can reduce drain time by 10x.
Testing the Ugly Scenarios
You must test under failure conditions, not just happy paths. A store-and-forward implementation that hasn’t been tested under real failures will lose data the moment something goes wrong. Here’s what to cover:
Simulate gateway outages at varied durations: 5 seconds, 5 minutes, 5 hours. Verify data integrity and ordering at each scale.
Test the buffer wraparound boundary. Fill the RAM ring buffer to exactly capacity, then one more record. Confirm it spills to flash cleanly.
Power-kill testing is non-negotiable. Pull power mid-flash-write. On boot, verify the flash state is consistent and no corrupt records leak into the drain. A CRC on every record catches this.
Verify duplicate handling: force a reconnect mid-drain and confirm the gateway sees the same sequence range a second time without creating duplicates.
If timestamps matter for your application (they usually do), sync time with the gateway on reconnect before starting the drain. Clock drift on a 32kHz crystal is roughly 20 to 100 ppm, which works out to 1 to 9 seconds of drift per day. That’s enough to matter.
Use RTT logging or a secondary UART to observe buffer state during testing. Don’t use the BLE link itself for debug output; it’ll mess with your timing.
Integrating Store-and-Forward Into Your Device
Store-and-forward on BLE is a firmware responsibility. BLE won’t queue your data, won’t retry your transmissions, and won’t tell you if the application on the other side actually got your payload.
The two-tier buffer (RAM ring buffer spilling to flash) handles the storage. Sequence numbers and a simple ACK protocol handle the forwarding. Exponential backoff on advertising handles the power budget during outages.
Size your buffers based on real outage data from your deployment environment, not guesses. If you don’t have real data yet, instrument your first batch of devices to log disconnection events and durations. You’ll probably be surprised at how often and how long connections drop.
If you’re starting a new device integration, Hubble’s device integration guide covers the broader architecture for getting BLE peripherals connected through to the cloud. The store-and-forward pattern bolts directly onto that foundation, giving you the resilience layer between your sensor and whatever lies upstream.
Hubble Network enables reliable sensor-to-cloud connectivity over BLE—even when gateways drop. See how it works →