Symptom
Values longer than 20 bytes arrive truncated, or the stack returns an error when you notify or write them. Often only happens with one phone or one central.
Why
The default ATT MTU is 23 bytes. The ATT header of a notification or write takes 3 bytes, leaving 20 bytes of payload. Nothing larger is sent until both sides agree on a bigger MTU in an Exchange MTU procedure.
Two different limits are involved:
| Layer | Default | Extended |
|---|---|---|
| ATT MTU (GATT payload + 3 bytes) | 23 | negotiated; stacks commonly allow up to 517 (512-byte attribute + header) |
| Link-layer payload | 27 bytes | up to 251 bytes with LE Data Length Extension (Bluetooth 4.2+) |
A large MTU without Data Length Extension still works, but the stack fragments each ATT packet into several 27-byte link-layer packets. With both enabled, one packet can carry up to 244 bytes of ATT payload (251 minus L2CAP and ATT headers).
Fix
- Request a larger MTU from the client right after connecting (Android:
requestMtu(); iOS negotiates automatically. ReadmaximumWriteValueLength(for:)instead of assuming). - Enable Data Length Extension in the peripheral stack configuration and prefer 2M PHY for throughput.
- Read back the negotiated MTU and size your packets to it. Never hard-code the MTU you asked for.
- Keep a fallback for 20-byte chunks, because some centrals still end up at 23.
Throughput reality
Measured application throughput ranges from roughly 0.2 Mbps with default settings on 1M PHY to about 1.4 Mbps with 2M PHY, DLE, large MTU and short intervals (Novel Bits measurements). Phones rarely reach the upper end.