R&D: the Bluetooth GATT cache bug that broke every second session
SHORT ANSWER
Why did a new Bluetooth service UUID per session stop the second connection from working?
Bluetooth centrals such as macOS CoreBluetooth cache a device's GATT service table by its address. A new random service UUID on the second session was never rediscovered, because the central kept using the cached table. The fix was one fixed service UUID per feature, with the session identified by a characteristic read fresh each time.
Key takeaways
- Randomising the service UUID per session looks safer and breaks reconnects. The service table is cached; characteristic values are not.
- Identify the session with a characteristic the central reads on every connection, not with the service UUID.
- Keep advertisements tiny: a device name pushed the legacy 31-byte payload over its limit and advertising failed.
- Two features should use two different fixed UUIDs, so one never matches the other.
The setup
The phone acts as a Bluetooth Low Energy peripheral. It advertises a service, a browser (through Web Bluetooth) finds it, connects, and exchanges messages through characteristics. For each session we generated a random session ID, and our first version also generated a random service UUID for it, on the theory that a unique service per session could not be confused with an old one.
The symptom
The first session worked. A second session with the same phone and the same computer did not connect: the new session's service UUID was never discovered by the computer. Nothing was wrong with the radio or the app's logic, which is what made it hard to see.
The cause
A central that connects to a peripheral discovers its services and then keeps the result. Platforms cache that table by the peripheral's address so later connections are fast. macOS CoreBluetooth, which we confirmed on real hardware, does this. When the peripheral later presents a different set of services, the central may keep using the table it already has, so the new UUID is never rediscovered.
Characteristic values are different: reading a characteristic goes to the device each time. The cache is about the structure, not the data.
The fix
- Use one fixed service UUID per feature, never one per session.
- Put the session ID in a readable characteristic. The central reads it fresh on every connection and compares it with the ID in the QR code.
- Give each feature its own fixed UUID, so a browser looking for one feature never matches a phone that is mid-flow in another.
- Make the browser-side script accept both the fixed UUID with the session check and, for older builds, the per-session UUID.
A second lesson: the advertisement
A BLE legacy advertisement carries 31 bytes. Adding the device name pushed it over and Android reported that the advertise data was too large, so nothing was advertised at all. Only the service UUID needs to be in the advertisement. Characteristics do not, and neither does the name. We confirmed both on real hardware.
Frequently asked questions
Is a fixed service UUID a privacy problem?
It is visible to anything that scans, so it identifies the feature, not the user. The session ID is read only after connecting. Use a feature-specific UUID, not one tied to a person.
Why not clear the cache from the browser?
Web pages cannot clear the platform's GATT cache, and you should not ask users to restart Bluetooth to reconnect.
Does Android cache the same way?
Android centrals have their own caching behaviour and an API to refresh it. We hit the problem on macOS and designed for the lowest common denominator.
References
Review status
Last technical self-review by the author on 4 October 2026. No independent reviewer yet. If you spot an error, write to [email protected] and we will correct it and note the change.
Rohan builds HostSpica's Android apps — Authenticator, Passkey and Identity — and writes up how they work, including the mistakes along the way.
ABOUT THE PRODUCTS
RELATED