Why Your First BLE Project Won't Connect (And How to Fix It)

You followed the tutorial exactly. You copied the example code, uploaded it to your ESP32, opened nRF Connect on your phone, and… nothing. No device. Or maybe it appears for a second, then vanishes. Or you tap “Connect” and watch it succeed, then fail, then succeed, then fail, a cruel little heartbeat of hope and disappointment. You’ve been at this for four hours. You’ve opened nineteen browser tabs. You’re starting to wonder if the board is broken, if the library is broken, or if you’re broken.
You’re none of those things. BLE is just genuinely one of the hardest “hello world” experiences in embedded development. Unlike blinking an LED, where one line of code gives instant feedback, Bluetooth Low Energy involves an advertising state machine, a connection negotiation, service discovery, characteristic permissions, descriptor configuration, and a phone OS that is actively lying to you about what it sees. Most developers’ first BLE project takes somewhere between three and ten times longer than they expected. That’s not a skills problem. That’s a tooling and knowledge problem.
Here’s the good news: the reasons your project isn’t working fall into about six categories, and every single one is fixable in minutes once you know what to look for.
How BLE Connection Actually Works (A 60-Second Mental Model)
Before getting into failure modes, you need a map. BLE communication follows a specific sequence, and your problem lives at one of these stages:
Advertising → Scanning → Connection → Service Discovery → Data Exchange
Your firmware tells the BLE radio to broadcast advertising packets. Your phone’s scanner app picks those up. You tap “Connect,” and the two devices negotiate a connection. Once connected, your phone discovers what services and characteristics your device offers (the GATT table). Only then can you actually read, write, or receive data.
The maddening part: a failure at any of these stages produces roughly the same symptom, “it won’t connect.” The fix for each is completely different. Let’s walk through them in order.
Your Device Isn’t Advertising (Or Not Advertising Correctly)
What you see: The device doesn’t appear in your scanner app at all. You’re staring at a list of nearby Bluetooth devices and yours isn’t one of them.
What’s actually wrong (in order of likelihood):
Advertising never started. This is more common than you’d think. On the ESP32 Arduino BLE library, you need to explicitly call pAdvertising->start() after setting up your services. If that call is buried inside a callback that never fires, or you simply forgot it, the radio is silent.
BLEDevice::init("MyDevice");
BLEServer *pServer = BLEDevice::createServer();
// ... set up services and characteristics ...
BLEAdvertising *pAdvertising = BLEDevice::getAdvertising();
pAdvertising->addServiceUUID(SERVICE_UUID);
pAdvertising->setScanResponse(true);
pAdvertising->start(); // THIS LINE. Is it in your code?
Wrong advertising type. BLE supports connectable and non-connectable advertising. If you’ve accidentally configured non-connectable advertising (common when copying beacon examples), your phone will see the device but the “Connect” button won’t work, or won’t appear at all.
Device name is too long. A BLE advertising packet is exactly 31 bytes. Your device name, service UUIDs, and flags all need to fit inside that. If your device name is “My_Super_Awesome_BLE_Temperature_Sensor_V2,” you’ve blown the budget. The advertising packet silently truncates or fails. Keep names under 15 characters during development.
TX power is too low. If you’re testing with the board across the room, a low transmit power setting means the signal never reaches your phone. Move the phone within one foot of the board. If it suddenly appears, that’s your answer.
Quick fix: Add a Serial.println("Advertising started") immediately after your start call. If you don’t see it, trace backward to find out why that code path isn’t executing. Then open nRF Connect and look at the RSSI value (signal strength) next to your device. Anything weaker than about -90 dBm is borderline.
Your Phone’s Bluetooth Cache Is Lying to You
What you see: The device appears but shows stale information. Or you tap Connect and nothing happens. Or you see a device name you changed in your code three iterations ago. Or you deleted your GATT services entirely but the phone still shows them.
What’s actually wrong: Both iOS and Android aggressively cache BLE GATT tables and bonding information. When your phone connects to a BLE device, it remembers the service and characteristic layout. When you change that layout in firmware and re-flash, your phone doesn’t know. It tries to use the old cached layout, and everything breaks silently.
This is arguably the single most common “invisible” cause of BLE frustration. It wastes more cumulative developer hours than any protocol bug.
How to fix it:
On Android: Go to Settings → Apps → Show System Apps → Bluetooth → Storage → Clear Cache. On some phones, simply toggling Bluetooth off and on isn’t enough. You need the full cache clear.
On iOS: Apple provides no user-facing way to clear the BLE cache. Your options are: toggle Bluetooth off, wait 10 seconds, toggle on. If that fails, forget the device in Settings → Bluetooth. If that fails, restart the phone. Yes, really.
Development workaround: During active development, change your device’s BLE MAC address or device name each time you modify the GATT table. This forces the phone to treat it as a brand-new device. On ESP32, BLEDevice::init("MyDev_v3") is the quick-and-dirty version.
Connection Drops Immediately After Connecting
What you see: nRF Connect shows “Connected” for one to three seconds, then “Disconnected.” Sometimes it reconnects and drops again in a loop.
What’s actually wrong:
No GATT services registered before advertising. If you start advertising before you’ve built your GATT table (services, characteristics, descriptors), the phone connects, tries service discovery, finds nothing, and gives up. The fix is structural: build everything first, advertise last.
// CORRECT ORDER:
BLEDevice::init("MyDevice");
BLEServer *pServer = BLEDevice::createServer();
BLEService *pService = pServer->createService(SERVICE_UUID);
// ... add characteristics to service ...
pService->start();
// NOW start advertising
BLEDevice::getAdvertising()->start();Your firmware is crashing after the connection event. When a BLE connection is established, the stack fires a callback. If your callback handler (or any code triggered by the connection event) causes a stack overflow or null pointer dereference, the firmware crashes and reboots. The radio disconnects. You don’t see a crash. You see a “disconnected” message on your phone.
Fix: Watch your serial monitor. If you see the ESP32 reboot message (a wall of register dump text) right after a connection attempt, you’ve found a crash. Common culprits: allocating large buffers on the stack inside callback functions, or calling BLE API functions that aren’t safe to use inside an interrupt context.
Supervision timeout is too short. The supervision timeout (part of BLE connection parameters) defines how long the devices wait without hearing from each other before declaring the connection dead. If it’s set too low (under 500ms), even minor radio interference causes a disconnect. Most SDKs default to reasonable values, but if you’ve been tuning connection parameters, check this. A value of 2000–4000ms is safe for development.
Connected but Service Discovery Comes Back Empty
What you see: The connection is stable, no immediate disconnect, but when you expand the device in nRF Connect, there are no services listed. Or you see “Unknown Service” where your custom service should be.
What’s actually wrong:
UUID format mismatch. This is a classic. BLE uses both 16-bit UUIDs (for standard services like Heart Rate, 0x180D) and 128-bit UUIDs (for custom services). If you defined a custom UUID as "12345678-1234-1234-1234-123456789abc" in your firmware but your phone app is searching for the 16-bit short form, it won’t match. Worse, some libraries expect the byte order reversed.
Double-check that you’re using the full 128-bit string format in your firmware and that it matches exactly what your client code (or nRF Connect’s filter) expects:
#define SERVICE_UUID "4fafc201-1fb5-459e-8fcc-c5c9c331914b"
#define CHARACTERISTIC_UUID "beb5483e-36e1-4688-b7f5-ea07361b26a8"Service created but never started. On ESP32, calling pServer->createService(SERVICE_UUID) allocates the service, but you must also call pService->start() before it becomes visible during service discovery. Missing this one line means the service exists in memory but is invisible to connected clients.
nRF52 note: In the nRF Connect SDK (Zephyr-based), services declared via the BT_GATT_SERVICE_DEFINE macro are automatically registered at boot. If you’re using the older nRF5 SDK, you need to call sd_ble_gatts_service_add() explicitly. Different SDKs, different foot-guns.
Reads Work but Writes Fail (or Notifications Never Arrive)
What you see: You can read a characteristic value but writing to it returns an error. Or you’ve set up notifications, subscribed on your phone, and… silence.
What’s actually wrong:
Characteristic properties are misconfigured. BLE characteristics have explicit property flags: READ, WRITE, WRITE_NR (write without response), NOTIFY, INDICATE. If you want the phone to write to a characteristic, the WRITE property must be set. This isn’t optional or inferred. It’s a bitfield you configure at creation time:
pCharacteristic = pService->createCharacteristic(
CHARACTERISTIC_UUID,
BLECharacteristic::PROPERTY_READ |
BLECharacteristic::PROPERTY_WRITE |
BLECharacteristic::PROPERTY_NOTIFY
);Missing CCCD for notifications. If you want to send notifications from your device to the phone, the characteristic needs a Client Characteristic Configuration Descriptor (CCCD). This is a small piece of metadata that allows the phone to “subscribe” to updates. On ESP32 Arduino, you add it like this:
pCharacteristic->addDescriptor(new BLE2902());Without that line, your phone has no mechanism to enable notifications, and pCharacteristic->notify() sends data into the void.
Permissions vs. properties confusion. Properties say what operations are possible. Permissions say what security level is required to perform them. If you’ve set a write permission that requires encryption but haven’t configured pairing, the write will be rejected with an “insufficient authentication” error that many scanner apps display unhelpfully as just “write failed.”
It Works on One Phone but Not Another
What you see: Everything is perfect on your Android test phone. Your friend tries with their iPhone and it doesn’t work at all. Or it works on your Pixel but not on your Samsung.
What’s actually wrong:
iOS enforces stricter BLE specification compliance than most Android devices. Advertising data that’s slightly malformed, like having flags in the wrong position or including duplicate data types, will be silently ignored by iOS while Android happily connects. If your device works on Android but is invisible on iOS, your advertising packet structure is likely non-compliant.
MTU (Maximum Transmission Unit) negotiation can also differ between phones. The default BLE MTU is 23 bytes (20 bytes of usable payload). Some phones negotiate higher MTUs automatically; others don’t. If your firmware assumes a large MTU without negotiating, data gets truncated on phones that stick to the default.
The fix: Test on at least two devices (one Android, one iOS) early and often. Don’t wait until demo day. Stick to spec-compliant advertising data structures. Both the ESP32 and nRF libraries will produce compliant packets if you use their high-level APIs and don’t hand-craft raw advertising bytes.
The Ten-Point Debugging Checklist
Print this out. Tape it to your monitor. Work through it in order before opening another browser tab:
- Is advertising actually starting? (Check serial log for confirmation.)
- Is the device visible in nRF Connect’s scanner?
- Have you cleared your phone’s Bluetooth cache since the last firmware change?
- Are all services and characteristics registered before advertising starts?
- Are your UUIDs in the correct format (128-bit string) and byte order?
- Are characteristic properties set correctly (READ/WRITE/NOTIFY)?
- Is a CCCD descriptor (BLE2902) present on every notify characteristic?
- Does serial output show a firmware crash after the connection event?
- Have you tested on a second phone (different OS if possible)?
- Are you running the latest version of your BLE library or SDK?
Nine times out of ten, your problem is one of these ten things. Work the list before you start questioning your life choices.
What to Do After You Get That First Connection
Once you’ve worked through this checklist and your device finally connects, stays connected, and exchanges data, take a breath. You just navigated one of the steepest learning curves in embedded development. Everything from here gets easier, because you now understand the state machine your code is fighting against.
Your next move: get comfortable with nRF Connect’s logging features. Tap the three-dot menu and enable verbose logging. This will show you exactly what’s happening during connection parameter negotiation, MTU exchange, and service discovery. Learning to read those logs now will save you dozens of hours on your next project.
BLE’s “hello world” is hard. But the distance between your first working connection and building something genuinely useful, a sensor that streams data to a dashboard, a controller that talks to a phone app, a wearable prototype, is much shorter than you think. The wall was getting here. You’re past it.
Hubble Network connects BLE devices directly to satellites—no gateways, no infrastructure, no range limits. See how it works →