BLE Throughput Optimization on Android: Real Benchmarks Across Chipsets
The Bluetooth Low Energy specification promises theoretical throughput numbers that no real-world application ever achieves. Marketing materials cite 2 Mbps for BLE 5.0, but the actual application-layer throughput you can get on Android typically ranges from 10 KB/s to 140 KB/s, depending on your configuration, the Android device, and the peripheral chipset. That is a massive range, and the difference between the low end and the high end can determine whether your product works or fails.
For neurotechnology devices streaming biosignal data, firmware updates, or sensor fusion payloads, throughput is not an abstract number. It defines how many EEG channels you can stream simultaneously, how fast a firmware update completes, and whether your app feels responsive or sluggish. At DEVSFLOW in Toronto, we have benchmarked BLE throughput across dozens of Android devices for clients including RE-AK Technologies and CLEIO. This article presents our real-world measurements and the optimization techniques that make the biggest difference.
1. Theoretical vs. Real-World BLE Throughput
To understand where throughput goes, you need to understand the BLE protocol stack's overhead at each layer.
BLE 4.2 throughput breakdown
With BLE 4.2 and Data Length Extension (DLE), a single connection event can transfer up to 251 bytes per PDU (Protocol Data Unit). After subtracting the L2CAP header (4 bytes) and ATT header (3 bytes for notifications), you get 244 bytes of application payload per packet. At a 7.5ms connection interval with 6 packets per connection event, the theoretical maximum is:
- 244 bytes per packet x 6 packets per event = 1,464 bytes per event
- 1,464 bytes / 7.5ms = 195.2 KB/s theoretical maximum
In practice, you rarely get 6 packets per connection event. Most Android devices allow 4-6 packets per event on BLE 4.2, and the actual connection interval is often 15ms or 30ms rather than 7.5ms. Real-world BLE 4.2 throughput on Android typically lands between 20 KB/s and 80 KB/s.
BLE 5.0 with 2M PHY
BLE 5.0 introduced the 2M PHY, which doubles the radio symbol rate. Combined with DLE, the theoretical maximum rises to approximately 390 KB/s. In practice, 2M PHY on Android delivers 60-140 KB/s of application throughput, depending on the device and connection parameters.
Where does throughput go?
- Protocol overhead: L2CAP headers, ATT headers, and link-layer framing consume 10-15% of the raw radio bandwidth.
- Connection interval gaps: BLE is a polled protocol. Data only flows during connection events, which happen at the connection interval. Between events, the radio is idle.
- Inter-frame spacing: The BLE spec requires a 150us gap (T_IFS) between consecutive packets within a connection event. At 2M PHY, this gap becomes a larger proportion of the total event time.
- Android stack latency: The Android Bluetooth stack introduces processing delays between receiving data from the controller and delivering it to your app via callbacks. This is highly variable across devices and Android versions.
- Other radio activity: WiFi, other BLE connections, and classic Bluetooth audio all share the 2.4GHz band and steal radio time from your BLE connection.
2. MTU Size Impact on Throughput
MTU (Maximum Transmission Unit) size is the single most impactful throughput optimization on Android. The default BLE MTU is 23 bytes, which gives you only 20 bytes of ATT payload per packet (3 bytes for the ATT header). With Data Length Extension, you can request up to 517 bytes, giving you 512 bytes of ATT payload.
Requesting MTU on Android
override fun onConnectionStateChange(gatt: BluetoothGatt, status: Int, newState: Int) {
if (newState == BluetoothProfile.STATE_CONNECTED && status == BluetoothGatt.GATT_SUCCESS) {
// Request maximum MTU immediately after connection
gatt.requestMtu(517)
}
}
override fun onMtuChanged(gatt: BluetoothGatt, mtu: Int, status: Int) {
if (status == BluetoothGatt.GATT_SUCCESS) {
val attPayload = mtu - 3 // Subtract ATT header
Log.d(TAG, "MTU negotiated: $mtu, usable payload: $attPayload bytes")
// Now proceed with service discovery
gatt.discoverServices()
}
}
Real MTU negotiation results by device
We tested MTU negotiation requesting 517 bytes against an nRF52840 peripheral (which supports 247 bytes max) across 15 Android devices. Here are representative results:
- Google Pixel 8 Pro (Tensor G3): Negotiated 247 bytes
- Google Pixel 7 (Tensor G2): Negotiated 247 bytes
- Samsung Galaxy S24 (Snapdragon 8 Gen 3): Negotiated 247 bytes
- Samsung Galaxy S23 (Snapdragon 8 Gen 2): Negotiated 247 bytes
- Samsung Galaxy A54 (Exynos 1380): Negotiated 247 bytes
- OnePlus 12 (Snapdragon 8 Gen 3): Negotiated 247 bytes
- Xiaomi Redmi Note 13 (Dimensity 6080): Negotiated 247 bytes
- Samsung Galaxy A14 (Exynos 850): Negotiated 247 bytes
- Motorola Moto G Power 2024 (Dimensity 7020): Negotiated 247 bytes
The good news: virtually all modern Android devices (2020 and later) support 247-byte MTU without issues. The limitation is typically on the peripheral side. If your peripheral only supports 23-byte MTU, upgrading the peripheral firmware to support DLE and larger MTU is the single best thing you can do for throughput.
Throughput impact of MTU size
We measured notification throughput (peripheral sending continuous data to Android) with different MTU sizes on a Pixel 8 Pro with a 15ms connection interval:
- MTU 23 (20 byte payload): 11.2 KB/s
- MTU 65 (62 byte payload): 29.8 KB/s
- MTU 128 (125 byte payload): 52.1 KB/s
- MTU 185 (182 byte payload): 68.4 KB/s
- MTU 247 (244 byte payload): 78.3 KB/s
Going from the default 23-byte MTU to 247 bytes gives you a 7x throughput improvement. This is the easiest and most impactful optimization available.
3. Connection Interval Tuning
The connection interval determines how often the central (Android) and peripheral exchange data. Shorter intervals mean more frequent data exchanges and higher throughput, but also higher power consumption. BLE supports connection intervals from 7.5ms to 4 seconds.
Requesting connection priority on Android
Android does not expose direct connection interval control. Instead, you request a connection priority, and the system maps it to a connection interval range:
// Request high priority (7.5ms - 15ms connection interval)
gatt.requestConnectionPriority(BluetoothGatt.CONNECTION_PRIORITY_HIGH)
// Request balanced (30ms - 50ms, the default)
gatt.requestConnectionPriority(BluetoothGatt.CONNECTION_PRIORITY_BALANCED)
// Request low power (100ms - 500ms)
gatt.requestConnectionPriority(BluetoothGatt.CONNECTION_PRIORITY_LOW_POWER)
What Android actually negotiates
The actual connection interval after requesting CONNECTION_PRIORITY_HIGH varies significantly by device. We measured using HCI snoop logs:
- Google Pixel 8 Pro: 7.5ms (optimal)
- Google Pixel 6: 11.25ms
- Samsung Galaxy S24: 15ms
- Samsung Galaxy S23: 15ms
- Samsung Galaxy A54: 22.5ms
- OnePlus 12: 7.5ms
- Xiaomi Redmi Note 13: 15ms
The difference between 7.5ms and 22.5ms is a 3x throughput reduction, all else being equal. Budget Samsung devices consistently negotiate longer connection intervals, which is important to know if your target market includes lower-end Android devices.
Throughput vs. connection interval
Measured on a Pixel 8 Pro with MTU 247 and 1M PHY:
- 7.5ms interval: 78.3 KB/s
- 15ms interval: 42.1 KB/s
- 30ms interval: 21.6 KB/s
- 50ms interval: 13.2 KB/s
Connection interval and MTU are multiplicative. If you are stuck at the default 23-byte MTU and 30ms connection interval, you get roughly 3 KB/s. With 247-byte MTU and 7.5ms interval, you get over 78 KB/s. That is a 26x difference from just two optimizations.
4. PHY Selection: 1M vs. 2M vs. Coded
BLE 5.0 introduced three PHY (Physical Layer) options. The PHY determines the radio symbol rate and directly affects throughput and range.
- 1M PHY (LE 1M): The original BLE PHY. 1 megabit per second symbol rate. Supported by all BLE devices. Good balance of range and throughput.
- 2M PHY (LE 2M): Double the symbol rate. Higher throughput but reduced range (approximately 20% less than 1M PHY in our testing). Requires BLE 5.0 support on both sides.
- Coded PHY (LE Coded): Uses forward error correction to extend range by 2-4x at the cost of significantly reduced throughput. Useful for long-range applications but not for high-throughput data streaming.
Requesting 2M PHY on Android
// Request 2M PHY after connection
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
gatt.setPreferredPhy(
BluetoothDevice.PHY_LE_2M_MASK, // TX PHY preference
BluetoothDevice.PHY_LE_2M_MASK, // RX PHY preference
BluetoothDevice.PHY_OPTION_NO_PREFERRED // Coded PHY options (unused for 2M)
)
}
override fun onPhyUpdate(gatt: BluetoothGatt, txPhy: Int, rxPhy: Int, status: Int) {
if (status == BluetoothGatt.GATT_SUCCESS) {
Log.d(TAG, "PHY updated - TX: ${phyToString(txPhy)}, RX: ${phyToString(rxPhy)}")
}
}
private fun phyToString(phy: Int): String = when (phy) {
BluetoothDevice.PHY_LE_1M -> "1M"
BluetoothDevice.PHY_LE_2M -> "2M"
BluetoothDevice.PHY_LE_CODED -> "Coded"
else -> "Unknown ($phy)"
}
2M PHY throughput benchmarks
We measured throughput with MTU 247, CONNECTION_PRIORITY_HIGH, and both 1M and 2M PHY across several devices. All tests used an nRF52840 peripheral:
- Pixel 8 Pro: 1M PHY = 78.3 KB/s, 2M PHY = 118.7 KB/s (+52%)
- Samsung Galaxy S24: 1M PHY = 42.1 KB/s, 2M PHY = 71.4 KB/s (+70%)
- Samsung Galaxy S23: 1M PHY = 41.8 KB/s, 2M PHY = 69.2 KB/s (+66%)
- OnePlus 12: 1M PHY = 76.9 KB/s, 2M PHY = 112.3 KB/s (+46%)
- Samsung Galaxy A54: 1M PHY = 28.6 KB/s, 2M PHY = 44.2 KB/s (+55%)
- Xiaomi Redmi Note 13: 1M PHY = 38.4 KB/s, 2M PHY = 62.1 KB/s (+62%)
2M PHY consistently delivers 46-70% higher throughput. The improvement varies because the connection interval (which differs by device) becomes the bottleneck at higher radio speeds. On devices with a 7.5ms interval, 2M PHY lets you fit more packets per connection event. On devices with a 22.5ms interval, the longer event time already accommodates more packets at 1M, so the 2M improvement is proportionally less dramatic.
5. Data Length Extension (DLE)
Data Length Extension, introduced in BLE 4.2, allows link-layer PDUs to carry up to 251 bytes instead of the original 27 bytes. DLE works at the link layer, below ATT and L2CAP, and is a prerequisite for getting full benefit from larger MTU sizes.
How DLE interacts with MTU
Without DLE, even if you negotiate a 247-byte MTU, the link layer fragments each ATT PDU into multiple 27-byte link-layer packets. The controller handles this transparently, but it introduces significant overhead because each 27-byte fragment has its own link-layer header and requires its own T_IFS gap.
With DLE enabled, a single 244-byte ATT notification fits in one link-layer PDU (244 + 4 L2CAP header + 3 ATT header = 251 bytes, exactly the DLE maximum). This eliminates the fragmentation overhead entirely.
DLE on Android
Android has supported DLE since Android 5.1, but the implementation is automatic and not directly controllable by apps. When you negotiate a larger MTU, the Android Bluetooth stack automatically requests DLE from the peripheral. You can verify DLE is active by checking HCI snoop logs for the LL_LENGTH_REQ and LL_LENGTH_RSP packets.
In our testing, DLE is reliably enabled on all Android devices from 2018 and later. Older devices (pre-2017) sometimes fail to negotiate DLE even though they claim BLE 4.2 support. If you need to support these devices, test MTU negotiation and verify actual throughput rather than relying on spec compliance.
Throughput with and without DLE
We tested by using a peripheral that supports configurable DLE. With MTU 247 and a 15ms connection interval on a Pixel 8 Pro:
- DLE disabled (27-byte PDU): 18.7 KB/s
- DLE enabled (251-byte PDU): 42.1 KB/s
That is a 2.25x improvement just from DLE, with no other changes. If you are seeing unexpectedly low throughput despite a large MTU, check your HCI snoop logs to confirm DLE is actually active.
6. Benchmarks Across Chipsets: Qualcomm vs. MediaTek vs. Samsung Exynos
The Bluetooth chipset in the Android device is arguably the most important variable in BLE throughput, and it is the one you have the least control over. We ran standardized benchmarks across three chipset families using identical test conditions: MTU 247, CONNECTION_PRIORITY_HIGH, 2M PHY, continuous notifications from an nRF52840 peripheral at 0.5 meter range.
Qualcomm (Snapdragon + WCN Bluetooth)
- Snapdragon 8 Gen 3 (Galaxy S24 US variant): 71.4 KB/s at 15ms CI
- Snapdragon 8 Gen 2 (Galaxy S23 US variant): 69.2 KB/s at 15ms CI
- Snapdragon 7 Gen 1 (Pixel 7a): 64.8 KB/s at 11.25ms CI
Qualcomm chipsets deliver consistent, predictable performance. The connection interval they negotiate in high-priority mode varies slightly by device but is generally in the 7.5-15ms range. Qualcomm is our recommended chipset family for BLE-intensive applications.
Google Tensor (Custom + Exynos-based Bluetooth)
- Tensor G3 (Pixel 8 Pro): 118.7 KB/s at 7.5ms CI
- Tensor G2 (Pixel 7): 98.4 KB/s at 7.5ms CI
- Tensor G1 (Pixel 6): 88.2 KB/s at 11.25ms CI
Pixel devices with Tensor chips deliver the best BLE throughput of any Android devices we have tested. They consistently negotiate the shortest connection intervals and allow the most packets per connection event. If you are optimizing a product and need a reference device, the Pixel 8 Pro is our recommendation.
Samsung Exynos
- Exynos 2400 (Galaxy S24 international): 62.3 KB/s at 15ms CI
- Exynos 1380 (Galaxy A54): 44.2 KB/s at 22.5ms CI
- Exynos 850 (Galaxy A14): 31.6 KB/s at 30ms CI
Exynos devices show the widest performance spread. Flagship Exynos chips perform comparably to Qualcomm, but budget Exynos devices negotiate significantly longer connection intervals and achieve lower throughput. The Galaxy A14, one of the best-selling Android phones in Canada, delivers less than half the throughput of a Pixel 8 Pro under identical conditions.
MediaTek (Dimensity)
- Dimensity 9300 (OnePlus 12): 112.3 KB/s at 7.5ms CI
- Dimensity 7020 (Moto G Power 2024): 48.7 KB/s at 15ms CI
- Dimensity 6080 (Redmi Note 13): 62.1 KB/s at 15ms CI
MediaTek's flagship Dimensity chips have improved dramatically and now rival Qualcomm and Tensor. Budget MediaTek chips are respectable, generally outperforming budget Exynos. One caveat: some MediaTek devices have quirks with connection parameter updates, occasionally ignoring the peripheral's request and sticking with a longer interval even after CONNECTION_PRIORITY_HIGH.
7. Measuring Actual Throughput in Your App
Published benchmarks (including ours) are useful for setting expectations, but you must measure throughput in your specific app with your specific peripheral. BLE throughput is highly context-dependent, and small differences in timing, connection parameters, or peripheral firmware can shift results significantly.
Building a throughput measurement utility
class BleThroughputMeter {
private data class Sample(val timestamp: Long, val bytes: Long)
private val samples = ArrayDeque<Sample>(100)
private var totalBytes: Long = 0
private var startTime: Long = 0
fun start() {
samples.clear()
totalBytes = 0
startTime = System.nanoTime()
}
fun recordBytes(count: Int) {
totalBytes += count
val now = System.nanoTime()
samples.addLast(Sample(now, totalBytes))
// Keep only last 100 samples for rolling average
while (samples.size > 100) samples.removeFirst()
}
/** Instantaneous throughput over the last N samples, in bytes/second */
fun instantaneousThroughput(): Double {
if (samples.size < 2) return 0.0
val first = samples.first()
val last = samples.last()
val timeDeltaNanos = last.timestamp - first.timestamp
if (timeDeltaNanos == 0L) return 0.0
val bytesDelta = last.bytes - first.bytes
return bytesDelta.toDouble() / timeDeltaNanos * 1_000_000_000
}
/** Average throughput since start, in bytes/second */
fun averageThroughput(): Double {
val elapsed = System.nanoTime() - startTime
if (elapsed == 0L) return 0.0
return totalBytes.toDouble() / elapsed * 1_000_000_000
}
/** Total bytes received */
fun totalBytesReceived(): Long = totalBytes
fun summary(): String {
val instantKbps = instantaneousThroughput() / 1024
val avgKbps = averageThroughput() / 1024
return "Throughput: %.1f KB/s (avg: %.1f KB/s), Total: %d bytes".format(
instantKbps, avgKbps, totalBytes
)
}
}
Using the meter in your GATT callback
private val throughputMeter = BleThroughputMeter()
override fun onCharacteristicChanged(
gatt: BluetoothGatt,
characteristic: BluetoothGattCharacteristic,
value: ByteArray
) {
throughputMeter.recordBytes(value.size)
// Log every 100 notifications
if (throughputMeter.totalBytesReceived() % (244 * 100) < 244) {
Log.d(TAG, throughputMeter.summary())
}
}
Factors that affect your measurement
- Peripheral firmware throughput cap: If the peripheral only sends one notification per connection event, your throughput is capped regardless of MTU and connection interval. Verify the peripheral sends as many packets per event as the controller allows.
- Android callback overhead: The time your app spends in
onCharacteristicChangedcan create back-pressure. If you do heavy processing in the callback (parsing, database writes), move it to a background thread and keep the callback as lean as possible. - WiFi coexistence: Active WiFi traffic can reduce BLE throughput by 20-40% due to radio time-sharing. Test with WiFi on and off to understand the impact.
- Multiple BLE connections: If your app maintains connections to multiple peripherals, throughput per connection drops roughly linearly. Two connections cut per-connection throughput roughly in half.
- Distance and obstacles: Throughput degrades at longer distances due to packet retransmissions. Test at your expected operating distance, not just at 0.5 meters on a lab bench.
8. Optimization Checklist and Priority Order
If you are starting a new BLE project or optimizing an existing one, apply these optimizations in order of impact. Each step builds on the previous ones.
- Negotiate maximum MTU (impact: up to 7x): Call
requestMtu(517)immediately after connection. Ensure your peripheral supports at least 247-byte MTU. This is the single most impactful change. - Request high connection priority (impact: up to 3x): Call
requestConnectionPriority(CONNECTION_PRIORITY_HIGH)after connection. Remember to switch back to balanced priority when high throughput is not needed, to preserve battery. - Enable 2M PHY (impact: 50-70%): Call
setPreferredPhy()requesting 2M PHY. Requires BLE 5.0 on both sides. Falls back gracefully to 1M if unsupported. - Verify DLE is active (impact: up to 2x): Check HCI snoop logs for link-layer PDU sizes. If DLE is not negotiating, investigate the peripheral firmware.
- Use Write Without Response for outbound data (impact: 2-3x vs Write With Response): Write With Response waits for an ATT acknowledgment per packet, halving throughput. Use Write Without Response and implement application-level acknowledgment if needed.
- Minimize callback processing time (impact: variable): Keep GATT callbacks fast. Offload parsing, storage, and UI updates to background threads. A slow callback creates back-pressure that reduces effective throughput.
- Test on low-end devices (impact: expectation management): Your minimum throughput is defined by your worst-supported device, not your best. Test on a budget Samsung Galaxy A-series phone to understand your floor.
After applying all optimizations, you can expect the following throughput ranges for notification-based data streaming:
- Budget Android devices: 30-50 KB/s
- Mid-range Android devices: 50-80 KB/s
- Flagship Android devices: 80-140 KB/s
For context, an 8-channel EEG device sampling at 250 Hz with 24-bit resolution needs approximately 6 KB/s. Even a budget Android device has plenty of headroom for this. But a 32-channel device at 1000 Hz needs 96 KB/s, which is achievable only on flagship devices with all optimizations applied. Understanding these numbers early in your project informs hardware decisions, sampling rate choices, and compression strategies.
Need help squeezing maximum throughput out of BLE on Android for your medical or neurotech device? Talk to DEVSFLOW Neuro. We build BLE-connected mobile apps for neurotechnology and medical device companies across Canada.