The negotiation that isn't

Bluetooth Low Energy has a connection parameter negotiation mechanism. Your peripheral firmware politely requests a connection interval, a slave latency, and a supervision timeout. The central device (the phone) considers your request and responds with what it's actually going to give you.

This is described in the Bluetooth specification as a "negotiation." This is generous. It's more like asking your landlord to lower the rent. Technically you can ask. Technically they can say no. And they will.

Your peripheral requests a 15ms connection interval because you need responsive data transfer. iOS says "that's cute" and gives you 30ms. On a good day. Sometimes 45ms. The spec says the central "should" honor reasonable requests, but "should" in a Bluetooth specification is doing approximately zero heavy lifting.

Android is a different kind of unpredictable. It might say "sure, 15ms!" and then actually deliver intervals that wander between 7.5ms and 45ms depending on what other apps are doing with Bluetooth, whether WiFi is actively transmitting, and possibly the phase of the moon. I've logged connection intervals on a Pixel that changed six times in ten seconds with no parameter update request.

You can set your connection parameters. You cannot control them. Plan accordingly.

MTU: the negotiation that almost works

The default BLE ATT MTU is 23 bytes. After the 3-byte ATT header, that leaves you 20 bytes of payload per packet. Twenty bytes. In 2026. My first modem was faster.

So you negotiate a larger MTU. Your peripheral says "I support 512 bytes." The phone responds with... 185 bytes. Or 247. Or 512. Or 23, because the negotiation silently failed and neither side told you.

iOS is actually consistent here, which is rare praise for iOS BLE behavior. Modern iOS devices (iPhone 7 and later) reliably negotiate 251 bytes. Not 512, but at least it's predictable.

Android is the wildcard. Some phones agree to 512 and deliver 512. Some agree to 512 and then fragment at 185 bytes internally, so your application sees 512-byte writes but the radio sends them in 185-byte chunks with inter-packet delays. The throughput is identical to requesting 185 in the first place, but now you've added confusion.

Samsung phones in particular have their own MTU behavior that varies by One UI version. I've tested the same firmware against a Galaxy S21 and a Galaxy S23 and gotten different MTU negotiation results. Same chipset family, different BLE stack versions, different behavior.

Throughput: the spec vs reality

BLE 5.0 introduced the 2M PHY, doubling the air-interface data rate to 2 Mbps. The marketing materials will tell you this means BLE 5.0 throughput is up to 1.3 Mbps after protocol overhead.

Your app will see maybe 50 Kbps. On a good day. Let me explain where the other 1,250 Kbps went.

Connection intervals. Data can only be exchanged during connection events. With a 30ms connection interval, you get 33 connection events per second. Even if you fill every event with maximum-size packets, you're fundamentally limited by how often the radio talks.

MTU overhead. Every ATT packet carries headers. Every L2CAP frame carries headers. Every link layer packet carries headers. Your 251-byte MTU delivers about 244 bytes of application data after the protocol stack takes its cut.

Radio time sharing. The phone's Bluetooth radio is shared with WiFi. On most phones, they're on the same chip (a "combo chip"). When WiFi is actively transmitting, BLE gets deprioritized. I've measured 60% throughput drops during a large WiFi download on an Android phone. The BLE spec doesn't mention this because it's an implementation detail. It's also the dominant factor in real-world throughput.

Other BLE connections. Your smartwatch, your wireless earbuds, your fitness tracker. Each active BLE connection shares the radio's connection event schedule. Three other devices means fewer slots for your peripheral.

For the medtech companion app, we designed the protocol for 20 Kbps sustained throughput. Not because that's all BLE can do, but because that's what we could guarantee across all target devices in real-world conditions. Designing for 50 Kbps and degrading to 20 Kbps gives a bad user experience. Designing for 20 Kbps and occasionally hitting 50 Kbps feels like a pleasant surprise.

The Android BLE bestiary

Android's BLE stack is not one stack. It's many stacks, each with their own personality.

Pixel phones use the AOSP Bluetooth stack (Fluoride/Gabeldorsche). It's the reference implementation. It's the most correct. It still has bugs, but they're well-documented bugs.

Samsung uses their own modifications on top of AOSP. Connection handling behaves differently. Background scanning has different power/performance tradeoffs. Some Samsung devices aggressively kill background BLE connections to save battery, even when your app has the correct foreground service declaration.

Xiaomi is a surprise every time. Different MIUI versions have different Bluetooth behaviors. The battery optimization system is especially aggressive and will kill your BLE service unless the user manually exempts your app. You cannot programmatically request this exemption.

And then there's GATT error 133. The infamous "GATT_ERROR" status code. Android's way of saying "something went wrong with the BLE operation, and I'm not going to tell you what." It can mean the device disconnected. It can mean the GATT cache is stale. It can mean the Bluetooth stack crashed internally and recovered. It can mean the device is out of range. The fix for GATT 133 is usually "disconnect, wait 500ms, reconnect, and hope." This is not a joke. This is production code in shipping applications.

The autoConnect parameter on connectGatt() deserves special mention. The documentation says it controls whether to "directly connect to the remote device (false) or to automatically connect as soon as the remote device becomes available (true)." What it actually controls varies by device. On some phones, autoConnect=true means the connection attempt has no timeout and will eventually succeed. On others, it times out after 30 seconds. On some Samsung devices, it behaves identically to false. Test on your target devices and document the behavior, because the documentation won't help you.

iOS background mode: the 15-minute prison

Your iOS app goes to the background. You've declared the bluetooth-central background mode. You've implemented state preservation and restoration. Apple's documentation says your app can maintain BLE connections in the background.

Here's what actually happens. iOS gives your backgrounded app approximately 3 seconds of execution time every 15 minutes to handle BLE events. If data arrives from your peripheral between those windows, it's buffered by the system and delivered in a batch when your app gets its timeslice.

For a health monitoring device that sends data every 5 minutes, this means your app receives 3 readings in a burst every 15 minutes instead of one reading every 5 minutes. Your UI needs to handle this. Your data pipeline needs to handle this. Your user needs to understand why the "real-time" graph updates in chunks.

If the user force-quits your app (swipe up in the app switcher), your BLE connection is terminated immediately, and state restoration will not recover it. The user has to relaunch the app manually. You cannot work around this. You can only write clear user-facing text explaining that force-quitting breaks the connection.

State preservation and restoration itself is... optimistic in its reliability. It works most of the time on most devices. When it fails, it fails silently. Your app relaunches, calls retrievePeripherals(withIdentifiers:), and gets back an empty array. The system "forgot" the connection. You have to scan and reconnect from scratch.

Survival guide

After building BLE-connected products for medical, industrial, and consumer applications, here are the rules that keep things working.

Implement chunked transfer with CRC at the application layer. Don't trust BLE's link-layer integrity checks for application data. Implement your own chunking protocol with sequence numbers and CRC-32 on every chunk. When a chunk fails CRC, request retransmission. The link layer will catch most corruption, but "most" is not "all," and in medical applications, "most" is not good enough.

Never trust connection parameter requests. After connecting, measure the actual connection interval by timing notification deliveries. Log it. Alert if it deviates from your minimum requirement. Design your data protocol to work at the worst-case connection interval, not the requested one.

Test on at least 5 different Android devices. From different manufacturers. With different Android versions. With different chipsets. If your app works on a Pixel, a Samsung, a Xiaomi, a OnePlus, and a Motorola, it'll probably work on most other devices. Probably.

Implement a ring buffer on the peripheral. When the phone disconnects (and it will, because the user walked out of range, or iOS killed your background session, or Android decided to "optimize" your app's battery usage), the peripheral should keep collecting data into a ring buffer. When the phone reconnects, sync the buffer. This is not optional for any application where data continuity matters.

Use notifications, not indications, unless you need acknowledgment. BLE notifications are fire-and-forget. The peripheral sends data, and the central receives it (or doesn't). BLE indications require the central to send a confirmation for each packet before the next one can be sent. This round-trip halves your throughput. Use notifications for streaming data and reserve indications for critical state changes where you genuinely need confirmation.

Budget 3x your estimated development time for BLE. The protocol is straightforward on paper. The real-world behavior across devices, operating systems, and environments will consume the other two-thirds of your schedule. Every BLE project I've worked on has hit at least one device-specific bug that required a workaround. Plan for it.

BLE is a useful, capable protocol. It's also a protocol that will smile to your face while silently doing something completely different from what you asked. Treat its documentation as aspirational, test everything empirically, and keep a spreadsheet of device-specific quirks. You'll need it.