Engineering

BLE on Android: What the Documentation Does Not Tell You

Back to Blog

Why BLE on Android Is Harder Than iOS

CoreBluetooth on iOS is consistent and predictable. Android BLE is neither. The Android stack sits on top of Bluedroid (now Fluoride), which interacts with vendor-specific chipsets and drivers. Every OEM modifies the Bluetooth HAL. The same API call can produce different behavior on a Samsung Galaxy S24, a Xiaomi 14, and a Pixel 8.

We have shipped BLE integrations for medical device companies and IoT clients across dozens of device models. The official documentation covers about 60% of what you need. This article covers the rest.

Scanning: The 30-Second Limit

Starting with Android 7, the OS silently stops BLE scans that run for more than 30 seconds without a ScanFilter. No callback. No error. The scan just stops finding devices.

The fix: always attach ScanFilter objects. Filter by service UUID, device name, or manufacturer data. If you need an unfiltered scan, stop and restart it on a 25-second cycle. We use a coroutine-based timer that handles this automatically with a brief overlap window to avoid missing advertisements.

Connection Management: Queue Everything

The Android BLE stack is single-threaded for GATT operations. Issue a characteristic read while a write is in progress and one of them silently fails. The documentation barely mentions this.

The solution is a serial operation queue. Every GATT operation enters a FIFO queue. Each waits for its callback before the next one executes. We build this with a Kotlin Channel and a single coroutine consumer.

Queue every single GATT operation. Half the Android BLE bugs we have debugged trace back to concurrent operations on the GATT layer.

MTU Negotiation: Always Request It

The default BLE MTU on Android is 23 bytes, giving you 20 bytes of usable payload. Call requestMtu(512) immediately after connection. The peripheral negotiates down to what it supports. Most modern devices handle 247 or higher, giving you over 10x the throughput.

On some Samsung devices running Android 9 and 10, requesting MTU immediately after connection fails silently. We add a 600ms delay before the request. It is ugly. It works. Always read the actual negotiated value from onMtuChanged and use it to chunk your writes.

Android 12+ update: Starting with Android 12 (API 31), the BLE connection parameter management changed significantly. The system now handles connection interval negotiation more aggressively, and the 600ms delay workaround may not be sufficient on some devices. We now listen for onConnectionStateChange with state STATE_CONNECTED, then wait for service discovery to complete via onServicesDiscovered before requesting MTU. This callback-driven approach is more reliable than a fixed delay across Android 12, 13, 14, and 15.

The 133 Error (GATT_ERROR)

Status 133 is the most common and least helpful error in Android BLE. It maps to GATT_ERROR, a catch-all that can mean anything. Here is what triggers it most often:

Our policy: close the GATT, wait with exponential backoff starting at 1 second, reconnect up to 3 times. This resolves the majority of 133 failures in production.

Background BLE: Foreground Services and Doze

Without a foreground service, the OS kills your BLE connection within minutes of backgrounding. This is not optional. On Android 14 and above, declare foregroundServiceType="connectedDevice" and request the FOREGROUND_SERVICE_CONNECTED_DEVICE permission in your manifest. On older versions, "location" was the workaround.

Android 15 note: Android 15 introduced further restrictions on foreground service launches from the background. If your app needs to reconnect to a BLE device after a reboot or after being killed, you must use BOOT_COMPLETED broadcast receivers or WorkManager to restart the foreground service. Direct background launches of foreground services are now blocked unless the app holds specific exemptions.

Even with a foreground service, deep Doze can defer scans and drop connections when the screen is off and the device is stationary. The only reliable fix is guiding users to disable battery optimization for your app.

OEM Battery Killers

Samsung puts apps in "Sleeping" and "Deep Sleeping" lists that override standard Android. Users must add your app to "Never Sleeping." Xiaomi is worse. MIUI has a battery saver, auto-start manager, and background limiter. All three need configuration. We have watched Xiaomi kill a foreground service 30 seconds after screen-off because auto-start was disabled.

Huawei EMUI defaults to "automatic" app launch management, letting the OS kill your process at will. Users must enable three toggles manually. There is no programmatic fix for any of this. We maintain a mapping of OEM models to battery-settings deep-link intents and surface a guided setup screen on first launch.

Testing: Real Devices Only

The Android emulator does not support Bluetooth. Every BLE feature must be tested on physical hardware. We keep a device lab covering Pixel, Samsung, Xiaomi, OnePlus, and a budget Android Go device. For peripheral simulation, we use Nordic nRF52 dev kits running custom firmware that mimics the target GATT profile.

Our Approach: Abstraction Over Platform BLE

We build a platform abstraction layer that hides the Android BLE stack behind a clean, coroutine-based API. Scanning, connection management, operation queuing, MTU negotiation, retry logic, and OEM detection all live inside this layer. Client code interacts with a BleDevice interface exposing suspend functions. Errors are sealed classes. Connection state is a StateFlow.

When a new OEM quirk surfaces, we fix it in one place. When a new API level changes BLE behavior, the abstraction absorbs it. Application code stays focused on business logic.

If you are building a product that depends on BLE and want a team that has already navigated these problems, let's talk about your project.

Building a neurotechnology product that relies on BLE? We specialize in mobile apps for EEG headsets, biometric wearables, and multi-sensor streaming devices. See our neurotechnology work

Zawar Nadeem

Zawar Nadeem

Founder & Lead Android Engineer at DEVSFLOW Technologies