Why Your BLE Connection Drops Every 30 Seconds: Supervision Timeout Explained

Your device connects cleanly. RSSI looks fine. GATT discovery completes. Then, almost exactly 30 seconds later, the link drops. You reconnect. 30 seconds later, it drops again. The pattern is too consistent to be RF noise and too fast to be a bug you can step through.
If you’ve already googled “BLE disconnect reason 0x08” you’ve probably found a dozen forum posts telling you to raise the supervision timeout. Sometimes that’s the fix. Often it isn’t, and you’ll waste an afternoon tuning parameters that were never the problem.
This is a diagnostic path, not a tutorial. We’ll work the link layer, point out the two different things that cause 30-second drops (they look identical from userspace), and finish with a decision tree you can run through in 10 minutes. Examples are Nordic nRF Connect SDK and Zephyr, but the logic applies to ESP-IDF and SiLabs too.
First, Get the Disconnect Reason Code
Don’t touch a parameter until you’ve logged the HCI reason byte. It narrows the search from “anything in the Bluetooth stack” to one of four or five branches.
| Code | Name | What it usually means |
|---|---|---|
0x08 | Connection Timeout | Supervision timeout fired. No valid packet from peer for N×10ms. |
0x13 | Remote User Terminated | Peer (often the phone) chose to disconnect. App-level or OS reason. |
0x16 | Local Host Terminated | Your own stack called disconnect. Check your code paths. |
0x22 | LL Response Timeout | Link-layer control procedure (e.g. DLE, encryption) didn’t complete. |
0x3E | Failed to be Established | Connection never fully came up. Often addr type or brownout. |
In Zephyr, hook the callback:
static void disconnected(struct bt_conn *conn, uint8_t reason)
{
LOG_WRN("Disconnected, reason 0x%02x (%s)", reason,
bt_hci_err_to_str(reason));
}On Nordic SoftDevice:
case BLE_GAP_EVT_DISCONNECTED:
NRF_LOG_WARNING("Disconn reason 0x%02x",
p_ble_evt->evt.gap_evt.params.disconnected.reason);
break;If you see 0x08, supervision timeout is your branch. If you see 0x13 from an iPhone after exactly 30 seconds, jump to the ATT timeout section. Don’t guess.
Supervision Timeout: What’s Actually Happening
The supervision timeout is a link-layer timer. Every time the peripheral receives a valid packet from the central (even an empty one), the timer resets. If no valid packet arrives for connSupervisionTimeout × 10ms, the controller drops the link with reason 0x08.
Nordic SoftDevice defaults to roughly 4 seconds. Zephyr’s BT_GAP_INIT_CONN_PARAM_DEFAULT lands in a similar range. iOS and Android centrals frequently override whatever you request.
The constraint you must satisfy:
Supervision_Timeout > (1 + Peripheral_Latency) × Connection_Interval × 2If your conn interval is 50ms and peripheral latency is 30, the peripheral can legally skip 30 connection events. That’s 1.5 seconds of silence per cycle. With a 4-second supervision timeout you’ve got headroom. Push latency to 100 and the math gets tight.
What actually happens on the air:
|--ci--|--ci--|--ci--|--ci--|...|--ci--|
[pkt] [skip] [skip] [skip] [silence past timeout] -> 0x08
<----- peripheral latency window ----->
<-------- supervision timeout ---------->Why “exactly 30 seconds” pops up so often: it’s rarely 30s of supervision timeout. It’s usually a peripheral that goes to sleep, blocks its main loop on a long flash write, or fails to honor its own latency contract. The 30s number is the ATT timeout (more on that below), not the LL timeout.
The other trap: effective parameters ≠ requested parameters. The central has final say. You request a 4s supervision timeout, the central gives you 2s, you assume 4s is in force, you tune your sleep code accordingly, and you drop every 2s. Log the params you actually got from the connection parameter update event.
Setting and Negotiating Parameters Correctly
Zephyr:
struct bt_le_conn_param param = BT_LE_CONN_PARAM_INIT(
24, /* min interval: 24 * 1.25ms = 30ms */
40, /* max interval: 40 * 1.25ms = 50ms */
0, /* peripheral latency */
400 /* supervision timeout: 400 * 10ms = 4s */
);
int err = bt_conn_le_param_update(conn, ¶m);Then log what you actually got back:
static void le_param_updated(struct bt_conn *conn, uint16_t interval,
uint16_t latency, uint16_t timeout)
{
LOG_INF("Params updated: int=%u lat=%u to=%u",
interval, latency, timeout);
}Nordic SoftDevice:
ble_gap_conn_params_t params = {
.min_conn_interval = MSEC_TO_UNITS(30, UNIT_1_25_MS),
.max_conn_interval = MSEC_TO_UNITS(50, UNIT_1_25_MS),
.slave_latency = 0,
.conn_sup_timeout = MSEC_TO_UNITS(4000, UNIT_10_MS),
};
sd_ble_gap_conn_param_update(conn_handle, ¶ms);iOS will reject anything outside its accessory design rules: interval min ≥ 15ms, interval max ≤ 2s, latency ≤ 30, timeout ≤ 6s. On top of that, the relation interval_max × (latency + 2) ≤ timeout × 1000 / 2 must hold. Android is looser but vendor-dependent. If you’re connecting to a phone, design to Apple’s rules and assume the phone will silently cap you.
For production peripherals, picking parameters that work across both the central you control and the centrals you don’t takes real care. The Bluetooth implementation guide covers some of this for gateway scenarios.
When It’s NOT Supervision Timeout
ATT timeout (30 seconds, fixed by spec). Core Spec Vol 3 Part F mandates a 30-second ATT transaction timeout. If your peripheral sends a Read Request, Write Request, or any ATT request and the response never comes, the stack will tear down the connection at exactly 30 seconds. This is the most common cause of “exactly 30s” drops on stalled GATT operations. The disconnect reason on the peer side is often 0x13 (peer terminated) or 0x16 locally, not 0x08. If your timing is suspiciously round, suspect this before supervision timeout.
RF environment. 2.4GHz is crowded. Wi-Fi channel 6, microwaves, USB 3 hubs (yes, really). Log RSSI and CRC error counts. If RSSI is fine but you’re losing packets, look at the spectrum, not the parameters.
Power and brownout. If your peripheral resets mid-connection you’ll see 0x3E on the next attempt, or 0x08 because the peripheral stopped responding. Look for boot banners in your RTT log between drops. A reset that takes 200ms looks identical to a dropped link from the central’s perspective.
MTU / Data Length Extension mismatch. Rare but real. If the central initiates a DLE procedure your stack mishandles, you’ll get 0x22 (LL response timeout). Usually shows up on first connect after a firmware update.
Bonding and encryption. Encryption procedure timeouts during reconnect. Check your bond storage isn’t corrupted and that you’re not silently failing the LTK exchange.
The Diagnostic Tree
BLE drops every ~30s
│
▼
[Log HCI disconnect reason]
│
┌────┼─────────┬───────────┬──────────┐
▼ ▼ ▼ ▼ ▼
0x08 0x13/0x16 0x22 0x3E other
│ │ │ │
│ ▼ ▼ ▼
│ [Stalled [Check [Check addr
│ ATT req? DLE / type, brownout,
│ App-side MTU encryption on
│ disc?] exchange] reconnect]
│
▼
[Log EFFECTIVE conn params from
le_param_updated callback]
│
▼
[Is (1+latency) × interval × 2
< supervision_timeout ?]
│
┌────┴────┐
No Yes
│ │
▼ ▼
[Fix [Check peripheral sleep,
math] flash writes, main loop
blocking > conn_interval]Plain-language checklist to copy into your debug notes:
- Log the HCI reason code in your disconnect handler. Don’t skip this.
- Capture the effective connection parameters from the parameter-updated callback, not what you requested.
- Verify
(1 + latency) × interval × 2 < supervision_timeoutwith the effective values. - If reason is
0x13/0x16and timing is near 30s, audit every outstanding ATT request for a missing response. This is where most “exactly 30s” mysteries live. - Check RTT logs for unexpected reboots between drops. A silent reset masquerades as a link drop.
- Sniff the air with nRF Sniffer or a Sniffle dongle. The empty PDUs near the disconnect tell you whether silence was on the peripheral or central side.
- Confirm the central isn’t applying its own parameter rules (especially iOS).
- Only after all of the above, tune parameters.
Run the Reason Code, Then the Math, Then the RF
Most “30 second” drops fall into two buckets: supervision timeout firing because peripheral latency math was wrong or the device slept too long, and ATT timeouts on a GATT request that was never answered. The reason code separates them in 30 seconds of log review.
Get the reason code first. Do the math second. Look at the RF and power third. Tune parameters last, and only with the effective values in hand.
If you’re debugging this on a production fleet rather than a single board, the next two pieces worth your time are reading nRF Sniffer captures (so you can tell which side went silent) and BLE power profiling (so you know whether your peripheral is actually sleeping through its own connection events).
Hubble Network enables BLE devices to connect at global scale without managing supervision timeouts, gateway pairing, or connection drops in the field. See how it works →