Christopher Renshaw Notes

Field note · Android · BLE

Surviving GATT_ERROR 133 on Android BLE.

If you've shipped a Bluetooth Low Energy app on Android, you've met error 133. onConnectionStateChange status=133. onCharacteristicRead status=133. It's the error code that means nothing and ruins everything.

What 133 actually is

The Android Bluetooth stack defines GATT_ERROR = 0x85 (133) as a generic, undifferentiated failure. Internally it's the bucket every native-layer error gets dropped into when the stack can't or won't tell you what really happened. The HCI command failed, or the link disconnected before the operation completed, or the controller is in a degraded state — you get 133.

So the first thing to internalize: 133 is not a single bug. It's a category. The job isn't to "fix 133" — it's to be resilient to whichever underlying transient produced it.

The four real causes I've seen in production

  1. Stale connection state — the app thinks it's connected, the controller doesn't. Common after rapid disconnect/reconnect cycles or after the user toggles airplane mode. Fix: explicit close() on every BluetoothGatt handle the moment a connection state goes anything but CONNECTED, even on errors. Never reuse a gatt instance.
  2. Bonding race — you tried to read an encrypted characteristic before bonding completed. The controller responds with insufficient authentication; the JNI layer turns it into 133. Fix: gate read/write attempts on ACTION_BOND_STATE_CHANGED reaching BOND_BONDED.
  3. Resource exhaustion — too many simultaneous GATT clients, scan filter slots full, or the app is holding a wakelock on the radio. Fix: serialize. One GATT operation at a time, full stop. The Android BLE stack is single-threaded under the hood; pretend it is in your code too.
  4. Genuine link loss — the peripheral walked out of range, browned out, or the firmware crashed. Fix: backoff and rescan, don't immediately reconnect on the same MAC.

The pattern that worked

At Octane Fitness, we shipped the BLE stack across six commercial machine lines installed in gyms, hotels, universities, and military facilities. The console is on 12+ hours a day. We saw 133 every flavor it comes in.

What finally made the dashboard quiet was a small state machine — call it ConnectionGate — that owned every transition and refused to let the rest of the app skip a step.

// Single-flight gate. Every BLE op flows through this.
class ConnectionGate(private val dev: BluetoothDevice) {
    private val mutex = Mutex()
    private var gatt: BluetoothGatt? = null
    private var attempt = 0

    suspend fun <T> withConnection(block: suspend (BluetoothGatt) -> T): T = mutex.withLock {
        var last: Throwable? = null
        repeat(3) { i ->
            try {
                val g = gatt ?: connectFresh().also { gatt = it }
                return@withLock block(g)
            } catch (e: GattError) {
                last = e
                closeQuietly()
                val wait = (250L shl i).coerceAtMost(2_000L)
                delay(wait + Random.nextLong(100)) // jittered backoff
            }
        }
        throw last ?: IllegalStateException("unreachable")
    }

    private fun closeQuietly() {
        try { gatt?.disconnect(); gatt?.close() } catch (_: Throwable) {}
        gatt = null
    }
}

Three rules in that snippet, and they matter more than the code:

What didn't help

I tried two things early on that looked good in theory and made nothing better:

How to debug 133 when it happens

Use adb logcat -v threadtime -s BluetoothGatt:V BtGatt.ContextMap:V BtGatt.btif:V. The BtGatt.btif tag often carries the real underlying HCI status — look for an opcode and a status hex byte right before the 133 surfaces in your callback.

Common ones:

Each maps to a different real-world cause. Logging that mapping in your crash analytics gives you a much better signal than counting raw 133s.

Bottom line

You can't fix GATT_ERROR 133 because it's not one bug. You can engineer past it: serialize operations, close failed handles, gate on bond state, retry with jittered backoff. Do those four things and the long tail of "intermittent BLE issues" in your bug tracker quietly empties out.