Bluetooth Low Energy

What is the BLE MTU, and why does it limit message size?

The BLE MTU, properly the ATT_MTU, is the largest Attribute Protocol packet two connected Bluetooth LE devices will exchange. On LE the default is 23 bytes, and a notification or a write without response spends 3 of those on a header. Devices can negotiate a larger value, but any message longer than one packet still has to be split by the application and reassembled on the other side.

Learning objectives

After reading this article you will be able to:

  • Explain what the ATT_MTU caps and where its header bytes go
  • Describe how two devices negotiate a larger MTU with the Exchange MTU procedure
  • Describe how apps split longer messages into chunks that fit the reported size

What the MTU measures

MTU stands for maximum transmission unit. On a Bluetooth LE connection, the number that matters to apps is the ATT_MTU. The Attribute Protocol chapter of the Core Specification defines it as the maximum size of any packet sent between a client and a server.

Every read, write and notification in GATT travels as one Attribute Protocol packet, so the ATT_MTU caps how much data any single operation can move. The GATT chapter sets the default ATT_MTU for LE at 23 bytes, and both client and server must support at least that.

Where the bytes go

Part of each packet is protocol overhead. The ATT chapter gives the formats:

  • Notification. One byte of opcode and two bytes of attribute handle, then the value. The value can be up to ATT_MTU minus 3 bytes. Anything longer is cut off, and only the first ATT_MTU minus 3 bytes are sent.
  • Write without response. The same layout: up to ATT_MTU minus 3 bytes of value.
  • Read response. One byte of opcode, then up to ATT_MTU minus 1 bytes of value.

At the default of 23, that leaves 20 bytes for a notification or a write without response.

How devices agree on a bigger MTU

A client that can handle more than the default can run the Exchange MTU procedure. The GATT chapter describes it: the client sends the largest packet it can receive, the server replies with the largest it can receive, and both then use the smaller of the two. The procedure may run only once per connection. If either side reports a value below the default, the ATT_MTU stays at the default.

Two related numbers come up often. The ATT chapter caps a single attribute value at 512 bytes, and from Android 14 the Android stack asks for 517 bytes, as described below.

The radio does not have to carry the whole packet in one go. The L2CAP chapter allows lower layers to fragment a packet to fit their own size limits, and the receiver recombines the fragments. A large ATT_MTU therefore works even if each radio packet is smaller.

What the platforms do

  • Android. An app calls BluetoothGatt.requestMtu() and learns the result in onMtuChanged. The reference warns that for a write without response, data longer than the MTU is truncated. From Android 14, the stack requests 517 bytes the first time any GATT client asks and ignores later requests on that connection, and the final value is the smaller of 517 and what the remote device offers.
  • iOS and macOS. Apps read from Core Bluetooth how much fits. maximumWriteValueLength(for:) on a peripheral gives the maximum bytes for a single write of a given type. On the peripheral side, a central’s maximumUpdateValueLength caps notifications, and updateValue(_:for:onSubscribedCentrals:) truncates anything longer.

Always use the value the platform reports. The same phone can end up with a different MTU on each connection, because the result depends on what each peer supports.

Why it limits message size

A chat message, a JSON document or a sensor batch will often be longer than one packet, even with a negotiated MTU. GATT has procedures for long attribute values, prepared writes and blob reads, but they are bounded by the 512-byte attribute limit and add round trips. A common approach is for the app to do its own framing instead:

  1. Split the message into chunks that fit the reported write or notification size.
  2. Put a small header on each chunk, such as a message ID, a sequence number and a final-chunk flag.
  3. Send the chunks with writes in one direction and notifications in the other.
  4. Reassemble on the receiving side, and discard or re-request incomplete messages.

Three practical points follow. First, size chunks from the negotiated value, not a constant, and fall back to the default when the peer reports something too small. Second, a larger MTU means fewer headers per byte of data. Third, notifications and writes without response get no confirmation from the receiver, so anything that must arrive needs an acknowledgement at the application level.

The same chunking idea scales up. Files and other large payloads crossing several devices are broken into larger application-level pieces, then each piece is split again to fit each BLE link; how large files move across a mesh covers that layer.

An SDK that runs its own protocol over BLE has to handle all of this. The Offline Protocol mesh SDK tracks MTU agreement between peers and exposes an undersizedMtuReports diagnostic counter for when a peer reports an MTU too small to carry a fragment header and a conservative default is used.

Frequently asked questions

Why are my BLE writes cut off at 20 bytes?

Usually because the link is still using the default ATT_MTU of 23 bytes. A write without response carries the attribute value plus a 3-byte header, so only ATT_MTU minus 3 bytes of value fit. Negotiate a larger MTU, check the value your platform reports, and split longer messages.

What MTU should an Android app request?

Android's documentation says that from Android 14 the stack requests 517 bytes when the first GATT client calls requestMtu and ignores later requests on that connection. The result is the smaller of 517 and what the remote device offers, so read the value from the onMtuChanged callback instead of assuming it.

Sources

Build it with Offline Protocol

The Offline Protocol networking API reference documents getBleDiagnostics, whose undersizedMtuReports counter records when a peer reported an MTU too small to carry a fragment header and the SDK fell back to a conservative default.

Read the networking API reference