The negotiated ATT MTU is protocol detail. What a page needs is the payload size it can safely send without a response.
Status: ๐ง In Review
The 20-Byte Wall
A long-running Web Bluetooth failure looked arbitrary from JavaScript. A characteristic write larger than 20 bytes failed on Windows with a generic GATT operation failed for unknown reason, while similar code worked on other platforms.
The number comes from Bluetooth ATT: the default MTU is 23 bytes and a write command spends 3 bytes on its header, leaving a 20-byte payload. Connections can negotiate a larger MTU, but Web Bluetooth gave the page no reliable way to choose a payload size for writeValueWithoutResponse().
The result was defensive chunking: applications split everything into 20-byte writes even when the connection could carry much more.
Three old issues describe parts of the same gap:
- 40686244 - writes above 20 bytes failing on Windows.
- 40163619 - the missing Exchange MTU behavior, especially visible on Android.
- 40265040 - expose enough negotiated-MTU information for an application to size writes.
Why the API Changed Shape
The first version of this work exposed the raw negotiated ATT MTU. The useful question for a page, however, is not "what was the protocol MTU?" It is:
How many bytes may I pass to
writeValueWithoutResponse()on this characteristic?
That became a synchronous characteristic attribute:
const size = characteristic.maxWriteWithoutResponseSize;
const chunk = payload.slice(0, size);
await characteristic.writeValueWithoutResponse(chunk);
maxWriteWithoutResponseSize is seeded from the negotiated ATT MTU (normally MTU - 3) and updates when a platform reports an MTU change. The API exposes the practical limit rather than making every site know ATT header accounting.
- IN REVIEW Expose maxWriteWithoutResponseSize
Getting the Value from Each Platform
The web-facing property is small; obtaining the value consistently is not.
Windows, Android, macOS/CoreBluetooth, and Linux/ChromeOS all expose GATT connection state differently. On BlueZ, the negotiated MTU appears as the MTU property on org.bluez.GattCharacteristic1 (available since BlueZ 5.62). Chromium's generated system API first needs to know that property exists before the backend can observe it.
Android already requests a larger MTU. That earlier work solved negotiation, but not discovery by the page:
Testing with a Real Peripheral
A mock can check that a property exists. It cannot prove that a 244-byte write reaches a radio intact.
The MTU sampler uses an ESP32-C3 peripheral named dino tester. Its firmware reports the peer MTU and echoes the length, first byte, and last byte of each received write. That makes silent truncation observable.
The sampler has three tests:
Progressive JPEG over BLE
The page reads maxWriteWithoutResponseSize, then pulls an embedded progressive JPEG chunk by chunk over notifications. The image moves from blurry to sharp as scans arrive. This is a useful end-to-end workload: negotiation, notifications, reassembly, and progressive decoding all have to agree.
Attribute Conformance
The conformance page verifies that the attribute exists, is in range, matches the peripheral's peer-MTU accounting, remains stable across repeated reads, and predicts the practical write limit.
Write-Size Probe
The probe sends deterministic payloads on both write APIs at sizes around the old boundary and up through larger negotiated limits. The ESP32 echoes the actual received length so the page can detect both explicit failures and silent truncation.
On the tested Windows path with an effective MTU of 517, writes without response can use payloads up to 514 bytes rather than being hard-coded to 20.
Run the test build with:
--enable-features=NewBLEGattSessionHandling,WebBluetooth
The project page includes the firmware, focused test pages, and prebuilt Android APKs from the current CL.
The Takeaway
Exposing a protocol number would have been easy. Exposing the number application code can actually use is the better API.
maxWriteWithoutResponseSize keeps ATT bookkeeping inside the browser, follows negotiated changes from each platform backend, and lets a site choose efficient chunks without probing by failure.