v157 · miscellaneous · web bluetooth
Web Bluetooth: maxWriteWithoutResponseSize
Chrome 157 exposes BluetoothRemoteGATTServer.maxWriteWithoutResponseSize — the largest payload writeValueWithoutResponse() can send in a single ATT packet — plus a maxwritewithoutresponsesizechanged event when the connection renegotiates it. Until now the only safe move was to hard-code 20 bytes, which is the floor, not the truth: a modern connection often carries ten times that.
concepts
-
MTU probe
Pair a real device, connect its GATT server, and read the negotiated write size. Runs the actual
navigator.bluetooth.requestDevice()flow behind a user gesture, and reports the honest unsupported branch when the API, the adapter, or the permission is not there. -
Chunk planner
Give it a payload size and a write size — typed in, or read from a paired device — and it works out packet count, protocol overhead, and how much of the transfer is header rather than data. The 20-byte assumption is shown alongside, which is where the cost becomes obvious.
-
Firmware transfer simulator
Push a firmware image through a simulated link at a chosen MTU and connection interval and watch the packets go. Run the same image at 20 bytes and at the negotiated size side by side to see the difference in wall-clock time, not just in packet count.
-
Handling a size change mid-transfer
The size can change while you are using it. This is the listener, the re-chunk, and the bug you get without them: a queue built for the old size that starts silently truncating when the link renegotiates down.
why it shipped
An ATT write without response has to fit in one packet. Exceed the negotiated size and the write is rejected or, on some platforms, silently truncated — which is worse, because the transfer appears to succeed and the device receives corrupt data. With no way to ask, applications did one of three things: assume the 23-byte default ATT MTU (20 bytes of payload after the 3-byte header), probe by writing and watching for failures, or keep a table of per-platform guesses. All three leave throughput on the floor, because a negotiated MTU of 185 or 512 is common and would carry the same firmware image in a fraction of the packets.
Exposing the number turns a guess into a read, and the change event means the answer stays right when the connection renegotiates.
the API
const device = await navigator.bluetooth.requestDevice({
filters: [{ services: ["battery_service"] }],
});
const server = await device.gatt.connect();
// The largest payload one writeValueWithoutResponse() can carry.
console.log(server.maxWriteWithoutResponseSize); // e.g. 182
// It can change when the connection renegotiates.
server.addEventListener("maxwritewithoutresponsesizechanged", () => {
rechunk(server.maxWriteWithoutResponseSize);
});
The property is on the server, not the characteristic: it describes the connection, so every characteristic on that device shares it. Read it again after any reconnect — a new connection negotiates a new MTU.