BLE Connection Parameters on Android: Interval, Latency, and Supervision Timeout
If you have ever watched a BLE-connected EEG headset drop samples only on certain Android phones, the root cause almost always traces back to connection parameters. These three numbers (connection interval, slave latency, and supervision timeout) control how frequently the central and peripheral communicate, how many events the peripheral can skip, and when the stack decides a connection is lost. Getting them right is the difference between a rock-solid 250 Hz biosignal stream and a frustrating mess of gaps in your recording.
At our Toronto studio, we have shipped BLE firmware and Android apps for neurotechnology clients including RE-AK Technologies and CLEIO. Across those projects, connection parameter tuning has consistently been one of the highest-impact optimizations available. This article covers everything an Android developer needs to know: what each parameter does at the link layer, how to request changes from the central side, how peripherals can push updates, how to measure the parameters your connection actually got, and how to make smart tradeoffs between throughput and battery life.
What Connection Parameters Are and How They Work at the Link Layer
A BLE connection is not a persistent radio link. It is a series of short radio exchanges called connection events, scheduled at regular intervals. Three parameters define the timing of these events:
- Connection Interval (CI): The time between the start of two consecutive connection events, measured in units of 1.25 ms. The BLE spec allows values from 7.5 ms (6 units) to 4000 ms (3200 units). A CI of 7.5 ms means the central and peripheral exchange packets 133 times per second. A CI of 30 ms means roughly 33 exchanges per second.
- Slave Latency (SL): The number of consecutive connection events the peripheral is allowed to skip without the central considering the connection lost. If slave latency is 0, the peripheral must respond at every connection event. If slave latency is 4, the peripheral can sleep through up to 4 events in a row, only waking for the fifth.
- Supervision Timeout (ST): The maximum time between two successfully received packets before the connection is considered lost. Measured in units of 10 ms, with a range of 100 ms to 32 seconds. If neither side hears from the other within this window, both sides drop the connection.
These three values are related by a constraint in the BLE specification: the supervision timeout must be greater than (1 + slaveLatency) * connectionInterval * 2. This ensures that even if the peripheral uses its full slave latency allowance, the supervision timeout still covers at least two actual communication attempts. If you violate this constraint, the controller will reject the parameter update.
At the link layer, the central is the master of the connection schedule. It decides when connection events happen. The peripheral listens at the expected time and responds within the same event. Each connection event can carry multiple packets back and forth (up to 6 on BLE 4.2+ with data length extension), so a single event at a 7.5 ms interval can transfer a surprising amount of data.
Using requestConnectionPriority() on Android
Android exposes exactly one API for the central (your phone) to influence connection parameters: BluetoothGatt.requestConnectionPriority(). It accepts one of three constants:
// Request high throughput parameters
gatt.requestConnectionPriority(BluetoothGatt.CONNECTION_PRIORITY_HIGH)
// Request balanced parameters (default)
gatt.requestConnectionPriority(BluetoothGatt.CONNECTION_PRIORITY_BALANCED)
// Request low power parameters
gatt.requestConnectionPriority(BluetoothGatt.CONNECTION_PRIORITY_LOW_POWER)
Here is what these translate to in practice on most Android Bluetooth stacks:
- CONNECTION_PRIORITY_HIGH: CI of 11.25 ms to 15 ms, slave latency 0, supervision timeout around 2 seconds
- CONNECTION_PRIORITY_BALANCED: CI of 30 ms to 50 ms, slave latency 0, supervision timeout around 4 seconds
- CONNECTION_PRIORITY_LOW_POWER: CI of 100 ms to 125 ms, slave latency 2 to 4, supervision timeout around 6 seconds
The critical thing to understand is that requestConnectionPriority() is a suggestion, not a command. When you call it, Android's Bluetooth stack sends an LL_CONNECTION_PARAM_REQ to the peripheral's link layer controller. The peripheral's controller can accept, reject, or propose different values. Even if both sides agree, the Android stack may adjust the values based on its own policies, the number of active BLE connections, and system power state.
The method returns a boolean indicating whether the request was sent, not whether it was accepted. There is no callback that tells you the resulting parameters. This is one of the most common sources of confusion for developers new to BLE on Android.
When to Call It
Call requestConnectionPriority(CONNECTION_PRIORITY_HIGH) immediately after service discovery completes and before you begin streaming data. For a biosignal recording session, keep it high for the duration of the recording, then drop to balanced or low power when the session ends.
class MyGattCallback : BluetoothGattCallback() {
override fun onServicesDiscovered(gatt: BluetoothGatt, status: Int) {
if (status == BluetoothGatt.GATT_SUCCESS) {
// Request fast connection interval before enabling notifications
gatt.requestConnectionPriority(
BluetoothGatt.CONNECTION_PRIORITY_HIGH
)
// Small delay to let the parameter update propagate
scope.launch {
delay(200)
enableDataNotifications(gatt)
}
}
}
fun onRecordingSessionEnded(gatt: BluetoothGatt) {
// Drop to balanced when we no longer need high throughput
gatt.requestConnectionPriority(
BluetoothGatt.CONNECTION_PRIORITY_BALANCED
)
}
}
The 200 ms delay after requesting high priority is not strictly required by the API, but we have found it improves reliability on Samsung and Pixel devices. The connection parameter update is an asynchronous link layer operation, and enabling notifications before it completes can lead to the first few notification packets being sent at the old (slower) interval.
How the Peripheral Can Request Parameter Updates
The central is not the only side that can initiate parameter changes. The BLE specification provides two mechanisms for the peripheral to request an update:
- L2CAP Connection Parameter Update Request: This is the older mechanism, available since BLE 4.0. The peripheral sends a request over the L2CAP signaling channel with its preferred min interval, max interval, slave latency, and supervision timeout. The central can accept or reject.
- LL_CONNECTION_PARAM_REQ (BLE 4.1+): A link layer PDU that achieves the same thing but at a lower protocol layer, with faster negotiation. Most modern peripherals use this.
For neurotechnology firmware, this is extremely useful. Your EEG headset firmware can request the connection parameters it needs right after the connection is established, without waiting for the Android app to call requestConnectionPriority(). This makes the system more robust because the firmware knows its own data rate requirements.
A typical pattern in Nordic nRF SDK firmware:
// In your peripheral firmware (nRF SDK / Zephyr example)
static void on_connected(struct bt_conn *conn, uint8_t err) {
// Request 7.5 ms interval, 0 latency, 4 second timeout
struct bt_le_conn_param param = {
.interval_min = 6, // 6 * 1.25 ms = 7.5 ms
.interval_max = 12, // 12 * 1.25 ms = 15 ms
.latency = 0,
.timeout = 400 // 400 * 10 ms = 4 seconds
};
bt_conn_le_param_update(conn, ¶m);
}
On the Android side, you will see this arrive as a parameter update event that the Bluetooth stack handles automatically. You do not get a callback in BluetoothGattCallback when the peripheral requests a parameter change. Android's stack will accept or reject the request based on its internal policy, and you have no direct control over that decision.
This is why it is valuable to have both sides requesting parameters. If the Android side requests CONNECTION_PRIORITY_HIGH and the firmware side requests a 7.5 ms interval, the resulting negotiation is more likely to land on fast parameters than if only one side makes the request.
Measuring Your Actual Connection Parameters
Since requestConnectionPriority() gives you no feedback about what parameters the connection actually settled on, you need other ways to measure. Here are three approaches we use regularly.
HCI Snoop Logs
Enable Bluetooth HCI snoop logging in Android developer options, reproduce your connection, then pull the log and open it in Wireshark. Filter for btle.control.opcode == 0x00 (LL_CONNECTION_UPDATE_IND) or look for L2CAP connection parameter update packets. The parameters will be clearly visible in the decoded packet fields.
# Pull the HCI snoop log from the device
adb pull /data/misc/bluetooth/logs/btsnoop_hci.log .
# On newer Android versions, the log path may differ
adb shell dumpsys bluetooth_manager | grep "Log file"
Notification Timestamp Analysis
If you know the peripheral is sending one notification per connection event, you can infer the connection interval from the inter-arrival times of notifications on the Android side. Log timestamps in your onCharacteristicChanged callback and compute the deltas:
class ConnectionIntervalEstimator {
private val timestamps = mutableListOf<Long>()
private val maxSamples = 200
fun onNotificationReceived() {
val now = System.nanoTime()
timestamps.add(now)
if (timestamps.size > maxSamples) {
timestamps.removeAt(0)
}
}
fun estimateIntervalMs(): Double {
if (timestamps.size < 10) return -1.0
val deltas = timestamps.zipWithNext { a, b -> (b - a) / 1_000_000.0 }
// Use median to filter outliers from scheduling jitter
val sorted = deltas.sorted()
return sorted[sorted.size / 2]
}
}
This method is not perfectly precise because Android's callback scheduling adds jitter, but it reliably distinguishes between a 7.5 ms interval and a 30 ms interval. If you see a median delta of around 11 to 15 ms, you got high priority. If it is 30 to 50 ms, you are on balanced.
Using the Android Bluetooth Diagnostic API (Android 13+)
Starting with Android 13, BluetoothGatt provides getConnectionInfo() on some OEM implementations. However, this is not part of the public stable API as of early 2026. The HCI snoop log method remains the most reliable cross-device approach.
The Throughput vs. Power Consumption Tradeoff
Every connection event costs energy on both sides. The radio must wake up, synchronize, exchange packets, and shut down again. A shorter connection interval means more events per second, which means higher throughput but also higher power draw.
Here is a rough breakdown based on measurements we have taken with Nordic Power Profiler kits in our Toronto lab:
- 7.5 ms CI: ~133 events/sec. Average peripheral current draw around 2 to 4 mA (depending on payload size). Maximum practical throughput around 80-100 KB/s with DLE.
- 15 ms CI: ~67 events/sec. Average draw around 1 to 2 mA. Throughput around 40-60 KB/s.
- 30 ms CI: ~33 events/sec. Average draw around 0.5 to 1 mA. Throughput around 20-30 KB/s.
- 100 ms CI: ~10 events/sec. Average draw under 0.3 mA. Throughput around 5-10 KB/s.
For a typical 8-channel EEG at 250 Hz with 24-bit samples, you need roughly 6 KB/s of raw data throughput. A 30 ms connection interval handles this comfortably. But if you add impedance data, event markers, accelerometer streams, or battery status, the bandwidth requirements climb quickly. We generally recommend a 15 ms interval as the sweet spot for multi-channel biosignal recording: it gives enough headroom for bursts without excessive battery drain.
Slave latency can help with power consumption in asymmetric use cases. If the peripheral only needs to send data every 100 ms but you want the option to command it at 15 ms intervals, set CI to 15 ms with a slave latency of 6. The peripheral can sleep through 6 events (90 ms) and still wake for every seventh, effectively running at ~100 ms for power purposes while the central retains the ability to reach it quickly. The moment the peripheral has data to send, it wakes on the next scheduled event regardless of its latency allowance.
Supervision Timeout: When and Why Connections Drop
The supervision timeout defines the patience threshold. If neither the central nor the peripheral successfully receives a packet from the other within this window, both sides declare the connection lost and clean up their state.
A too-short supervision timeout causes spurious disconnections. The user walks to the next room, their body momentarily shields the antenna, and the connection drops after a brief interruption. A too-long supervision timeout means the app sits in a connected state for many seconds after the peripheral has actually powered off or moved out of range, creating a confusing user experience.
For medical and neurotechnology devices, we typically recommend a supervision timeout of 4 to 6 seconds. This survives brief RF interruptions (someone walking between the phone and headset) while still detecting genuine disconnections within a reasonable time frame.
Here is the gotcha: on many Android devices, the Bluetooth stack overrides your requested supervision timeout with its own preferred value. Samsung devices in particular tend to cap the supervision timeout at 2 seconds when CONNECTION_PRIORITY_HIGH is active. This can cause unexpected disconnections in environments with RF interference, like busy hospital floors or trade show venues.
If you are seeing disconnections with status = 8 (GATT_CONN_TIMEOUT) in your onConnectionStateChange callback, the supervision timeout is probably the culprit:
override fun onConnectionStateChange(
gatt: BluetoothGatt,
status: Int,
newState: Int
) {
when {
newState == BluetoothProfile.STATE_DISCONNECTED -> {
when (status) {
0 -> Log.d(TAG, "Clean disconnect (local or remote initiated)")
8 -> Log.w(TAG, "Supervision timeout - connection lost")
19 -> Log.d(TAG, "Remote device terminated connection")
22 -> Log.w(TAG, "Local host terminated - possible resource issue")
34 -> Log.w(TAG, "LMP response timeout")
62 -> Log.w(TAG, "Connection failed to establish")
else -> Log.w(TAG, "Disconnect with status: $status")
}
// For timeout disconnects, implement reconnection
if (status == 8) {
scheduleReconnection(gatt.device)
}
}
}
}
Parameter Negotiation: How Central and Peripheral Reach Agreement
The parameter negotiation process is one of the least well-documented parts of BLE, partly because it happens at the link layer below what most application developers interact with. Here is the full sequence:
- Initial connection: The central selects initial parameters based on its own defaults. On Android, this is typically a 30 to 50 ms interval with balanced power settings.
- App request: Your Android app calls
requestConnectionPriority(). The stack translates this to an LL_CONNECTION_PARAM_REQ or an LL_CONNECTION_UPDATE_IND (depending on roles and BLE version). - Peripheral response: The peripheral's link layer controller either accepts the request, rejects it, or proposes alternatives. If it rejects, the parameters stay as they were.
- Peripheral-initiated request: The peripheral firmware can also send its own preferred parameters. Android's stack evaluates the request against its policy (current number of connections, power state, OEM-specific rules) and accepts or rejects.
- Ongoing adjustments: Some Android OEMs (notably Samsung and Huawei) periodically adjust connection parameters based on system load. If the phone is thermal throttling or has many active BLE connections, the stack may silently widen the connection interval.
The practical implication is that you should not assume your parameters will hold for the entire duration of a session. Build your app to be resilient to parameter changes. If your data stream relies on a specific minimum throughput, monitor the effective rate and alert the user (or re-request high priority) if it degrades.
class ThroughputMonitor(
private val gatt: BluetoothGatt,
private val minBytesPerSecond: Int = 5000
) {
private var bytesReceived = 0L
private var windowStart = System.currentTimeMillis()
private val windowMs = 3000L
fun onDataReceived(bytes: Int) {
bytesReceived += bytes
val elapsed = System.currentTimeMillis() - windowStart
if (elapsed >= windowMs) {
val bps = (bytesReceived * 1000) / elapsed
if (bps < minBytesPerSecond) {
Log.w(TAG, "Throughput dropped to $bps B/s, re-requesting high priority")
gatt.requestConnectionPriority(
BluetoothGatt.CONNECTION_PRIORITY_HIGH
)
}
bytesReceived = 0
windowStart = System.currentTimeMillis()
}
}
}
Device-Specific Behavior You Should Know About
Android's BLE stack behavior is not uniform across OEMs. Here are specific quirks we have documented while building BLE apps for Canadian neurotechnology companies:
- Samsung (Exynos): Tends to enforce a minimum CI of 11.25 ms even when 7.5 ms is requested. Supervision timeout is often capped at 2 seconds in high priority mode. Samsung devices also have a history of silently widening CI when the screen is off, even if your app holds a wake lock.
- Google Pixel (Tensor): Generally the most predictable behavior. Honors CI requests down to 7.5 ms. Supervision timeout follows the requested value up to about 6 seconds. Connection parameter updates from the peripheral side are reliably accepted.
- Xiaomi / Redmi (Qualcomm): Some models aggressively power-manage BLE connections after 30 seconds of screen-off time, regardless of connection priority requests. The workaround is to hold a partial wake lock and ensure your service runs as a foreground service with a notification.
- OnePlus / Oppo (Qualcomm): Generally well-behaved, but some older OxygenOS builds had a bug where calling
requestConnectionPriority()twice in quick succession would crash the Bluetooth stack. Adding a 500 ms debounce between calls avoids this. - Huawei (Kirin): Implements its own BLE parameter management layer on top of the standard stack. Connection intervals may be adjusted based on HarmonyOS-specific power profiles. Testing on Huawei hardware is essential if your user base includes these devices.
The fragmentation is real. For any serious BLE product, maintain a device compatibility matrix and test on at least 5 to 6 representative devices from different OEMs. We keep a test bench of 12 phones in our lab specifically for this purpose.
Practical Recommendations for Biosignal Streaming
Based on our experience shipping BLE-connected neurotechnology apps across Canada, here are our concrete recommendations for connection parameter management:
- Request high priority early and explicitly. Call
requestConnectionPriority(CONNECTION_PRIORITY_HIGH)right after service discovery. Do not rely on the peripheral's parameter update request alone, because some Android stacks are more likely to honor the central's own request. - Have the firmware request parameters too. Belt and suspenders. If both sides are asking for fast parameters, the negotiation is more likely to converge on what you need.
- Use a 15 ms interval for most biosignal applications. The 7.5 ms minimum gives diminishing throughput returns for the battery cost. A 15 ms interval provides 40-60 KB/s of practical throughput, which is more than enough for 8 to 32 channel EEG at 250 Hz plus auxiliary data.
- Set supervision timeout to 4 seconds. This survives brief RF interruptions without leaving the app in a zombie connected state for too long after a genuine disconnection.
- Monitor throughput at the application layer. Do not trust that your parameters will hold. Watch the data rate and re-request high priority if it drops. Log parameter anomalies for post-session analysis.
- Test on real hardware across OEMs. The emulator and even single-device testing will mislead you. Samsung, Pixel, and at least one Qualcomm-based device should be your minimum test matrix.
- Drop to balanced when idle. When the user stops a recording session, switch to CONNECTION_PRIORITY_BALANCED. This is good citizenship toward battery life and avoids the Android stack forcibly adjusting your parameters at inconvenient times.
Connection parameters are the foundation of BLE throughput and reliability. Getting them right will not solve every BLE problem, but getting them wrong will make every other optimization irrelevant. Start here, measure what you actually get, and build your buffering and error handling to tolerate the variation that Android's fragmented ecosystem inevitably introduces.
Building a BLE-connected medical or neurotechnology device and need help tuning your Android connection parameters? Talk to DEVSFLOW Neuro. We build BLE-connected mobile apps for neurotechnology and medical device companies across Canada.