Reliable BLE Data Transfer: Handling MTU, Throughput & Chunking

Sooner or later, every BLE developer runs into the same wall: you need to send more than 20 bytes at a time. Maybe it is a firmware image, a batch of sensor readings, or a configuration payload. You fire off a write and… only the first 20 bytes arrive. The rest is silently dropped.
The root of this problem is the MTU (Maximum Transmission Unit) — the maximum number of bytes a single BLE packet can carry. Understanding MTU, knowing how to negotiate it, and building a reliable chunking layer on top of it is essential for any real-world BLE application.
In this article we will cover everything you need to know: what MTU actually is, how to negotiate it on iOS and Android, the difference between write types, how to build a chunking protocol, and how to maximize throughput.
Let’s get started!
Foundational Knowledge
Before we dive into code, let’s build a clear mental model of how BLE data transfer works.
The ATT Layer
BLE data exchange happens through the ATT (Attribute Protocol) layer. When you read or write a characteristic, you are sending an ATT packet. Every ATT packet has a fixed overhead of 3 bytes (1 byte opcode + 2 bytes attribute handle), leaving the rest for your actual payload.
1 | ┌──────────────────────────────────────────┐ |
So the effective payload per write is:
Effective payload = MTU - 3 bytes
With the default MTU of 23 bytes, you get only 20 bytes of usable data per write. That is why 20 bytes is the magic number you see everywhere.
MTU vs Packet Length vs Throughput
These three concepts are related but different:
| Concept | What it means | Default |
|---|---|---|
| MTU | Max ATT payload per packet | 23 bytes |
| Data Length (DLE) | Max Link Layer payload (Bluetooth 4.2+) | 27 bytes, up to 251 |
| Throughput | Actual data transferred per second | Depends on all factors |
MTU is negotiated at the application level. Data Length Extension (DLE) is negotiated at the link layer. Both must be optimized for maximum throughput.
MTU Negotiation
The default BLE MTU is 23 bytes — designed in 2010 for tiny sensor readings. Modern BLE 4.2+ devices support MTU up to 517 bytes (the Bluetooth spec maximum). To unlock this, the central must explicitly request a larger MTU.
On iOS (CoreBluetooth)
iOS handles MTU negotiation automatically. When you connect to a peripheral, CoreBluetooth negotiates the highest MTU supported by both sides. You do not call any “request MTU” method — instead, you query the result:
1 | // After connecting and discovering characteristics |
There are two variants:
1 | // For write-without-response — returns the max payload directly |
Important: on iOS, you cannot set a specific MTU value. The system negotiates the maximum automatically. Starting from iOS 16, most devices negotiate 517 bytes MTU when the peripheral supports it.
On Android
Android requires an explicit MTU request:
1 | // After connecting to the GATT server |
Timing matters. Always request MTU after connection is established but before you start reading or writing characteristics. A common mistake is to request MTU too late, after the default 23-byte MTU has already been used for service discovery.
1 | // Recommended connection flow on Android |
iOS vs Android MTU Comparison
| Aspect | iOS | Android |
|---|---|---|
| How to request | Automatic | requestMtu(517) |
| Default | Negotiates max automatically | 23 until you request |
| Max supported | 517 | 517 |
| Query result | maximumWriteValueLength(for:) |
onMtuChanged callback |
| Common gotcha | None — just read the value | Forgetting to call requestMtu |
Write Types: With Response vs Without Response
BLE offers two write modes, and choosing the right one directly impacts reliability and throughput.
Write With Response (Acknowledged)
1 | Central Peripheral |
- The peripheral acknowledges every write.
- If a write fails, you know immediately.
- Slower — must wait for acknowledgment before sending the next packet.
- Max payload per write: min(MTU - 3, 512) bytes.
Write Without Response (Unacknowledged)
1 | Central Peripheral |
- No acknowledgment — fire and forget.
- Much faster — can queue multiple packets in a single connection event.
- Risk of packet loss if the peripheral’s buffer overflows.
- Max payload per write: (MTU - 3) bytes.
When to Use Each
| Scenario | Recommended Type |
|---|---|
| Configuration writes | With Response |
| Critical commands | With Response |
| Firmware update (OTA/DFU) | Without Response + app-level ACK |
| Streaming sensor data | Without Response |
| Large file transfer | Without Response + chunking protocol |
For high-throughput transfers, write-without-response is the way to go. But you need to build your own reliability layer on top — that is where chunking comes in.
Flow Control: Avoiding Buffer Overflow
When using write-without-response, the biggest risk is overflowing the BLE stack’s internal buffer. If you send packets faster than the radio can transmit them, packets get dropped silently.
iOS — canSendWriteWithoutResponse
CoreBluetooth provides a built-in flow control mechanism:
1 | func sendNextChunk() { |
This is the correct way to stream data on iOS. Never use timers or arbitrary delays — peripheralIsReady(toSendWriteWithoutResponse:) tells you exactly when the stack is ready for more data.
Android — Flow Control
On Android, writeCharacteristic returns false if the internal buffer is full. Additionally, you must wait for the onCharacteristicWrite callback before sending the next packet:
1 | private val writeQueue = ArrayDeque<ByteArray>() |
Common mistake: firing writes in a tight loop on Android without waiting for callbacks. This causes silent data loss that is extremely hard to debug.
Building a Chunking Protocol
When your payload exceeds the MTU, you need to split it into chunks, send them sequentially, and reassemble on the other side. Here is a practical protocol design.
Packet Format
1 | ┌─────────┬──────────┬─────────────────────┐ |
With a 517-byte MTU, each chunk carries up to 511 bytes of payload (517 - 3 ATT overhead - 3 protocol header).
iOS Implementation
1 | struct ChunkHeader { |
Receiver Side (Peripheral / Firmware)
The peripheral reassembles based on the flags and sequence number:
1 | 1. Receive chunk |
Maximizing Throughput
BLE throughput depends on multiple factors working together. Here is how to optimize each one.
1. Negotiate Maximum MTU
1 | MTU 23 → 20 bytes/packet → ~2.5 KB/s typical |
Always request 517. Even if the peripheral supports less, the negotiation settles on the highest common value.
2. Enable Data Length Extension (DLE)
DLE increases the Link Layer packet size from 27 to 251 bytes. This means fewer radio packets per ATT payload. On iOS, DLE is enabled automatically. On Android 5.0+, it is usually automatic after MTU negotiation, but some devices require:
1 | // Android — some devices need explicit DLE request |
3. Use Write Without Response
With-response writes are limited to one packet per connection event due to the ACK round-trip. Without-response writes can pack multiple packets into a single connection event.
1 | With Response: 1 packet × 514 bytes per event |
4. Request Shorter Connection Interval
The connection interval determines how often the central and peripheral exchange data. Shorter intervals mean more opportunities to send packets.
1 | Connection interval 30ms → ~33 events/second |
On iOS, you cannot set the connection interval directly — CoreBluetooth picks a value based on the peripheral’s preferred parameters. On Android:
1 | // Android — request high priority connection (shorter interval) |
Warning: shorter intervals increase power consumption significantly. Use CONNECTION_PRIORITY_HIGH only during active data transfer, then switch back:
1 | // After transfer completes |
5. Throughput Calculation
Theoretical maximum throughput:
1 | Throughput = (MTU - 3) × packets_per_event × (1000 / connection_interval_ms) |
In practice, expect 30-80 KB/s on iOS and 50-100 KB/s on Android with well-tuned parameters. Real-world factors like RF interference, other BLE connections, and peripheral firmware limitations reduce the theoretical maximum.
Common Pitfalls
1. Sending Without MTU Negotiation
1 | // Android — forgot to call requestMtu() |
Always negotiate MTU before any data transfer.
2. Writing in a Tight Loop
1 | // iOS — WRONG |
Use canSendWriteWithoutResponse and peripheralIsReady(toSendWriteWithoutResponse:) instead.
3. Ignoring Write Type Capability
Not every characteristic supports both write types. Always check:
1 | if characteristic.properties.contains(.writeWithoutResponse) { |
4. Hardcoding MTU Values
1 | // WRONG — hardcoded |
MTU varies between devices. Always query at runtime.
5. Not Handling Disconnection During Transfer
Long transfers can be interrupted by disconnection. Your chunking protocol should support resumption:
1 | class ResumeableTransfer { |
Best Practices Summary
- Always negotiate max MTU — call
requestMtu(517)on Android; on iOS it is automatic, just read the result. - Use write-without-response for bulk data — it is 4-6x faster than write-with-response.
- Respect flow control — use
canSendWriteWithoutResponseon iOS and wait for callbacks on Android. Never loop blindly. - Build a chunking protocol — include sequence numbers and start/end flags for reassembly and error recovery.
- Query MTU at runtime — never hardcode 20 bytes. Devices negotiate different MTU values.
- Request high connection priority during transfer — on Android, use
CONNECTION_PRIORITY_HIGHduring active transfer, then switch back to balanced. - Handle disconnection gracefully — support resumable transfers for large payloads.
- Test on real devices — simulators do not reflect real-world BLE behavior. Always validate throughput and reliability on physical hardware in noisy RF environments.
Summary
BLE data transfer is simple on the surface — write bytes to a characteristic — but getting it right requires understanding the full stack: MTU negotiation, write types, flow control, and chunking. The default 20-byte limit is not a hard wall, it is just the starting point.
With proper MTU negotiation (517 bytes), write-without-response, flow control, and a well-designed chunking protocol, you can achieve 30-100 KB/s of reliable throughput — more than enough for firmware updates, configuration files, and sensor data batches.
The key takeaway: never send BLE data without knowing your MTU, and never stream without flow control. Get these two things right, and the rest follows naturally.
Have a great weekend!