Hardware, IoT & Embedded · BLE

Bluetooth Low Energy Basics: Advertising, GATT, and Power

Design BLE devices that connect reliably and last longer.

Reading time: ~8–12 min
Level: All levels
Updated:

Bluetooth Low Energy (BLE) is deceptively simple: advertise, connect, read/write a few values, done. But reliability and battery life depend on details—what you put in advertising packets, how you model your GATT database, and how you choose connection parameters. This guide gives you the practical mental model and defaults that help a BLE device connect quickly and run longer on a small battery.


Quickstart

If you want results fast, do these in order. You’ll get: (1) a device that’s discoverable in seconds, (2) a GATT layout that’s easy to debug, and (3) radio settings that don’t drain your battery.

1) Decide the product behavior

Answer this first—every other decision flows from it.

  • Beacon-only? (no connection; data in advertisements)
  • Connectable? (phone/app reads data via GATT)
  • Push updates? (notifications) or pull? (reads)
  • Security needed? (pairing/bonding) or open access

2) Use “boring” advertising defaults

Boring is good in BLE. Fancy packets are where subtle bugs hide.

  • Include a short, stable device name
  • Advertise one primary Service UUID (or a short custom identifier)
  • Start with 250–1000 ms advertising interval
  • Use scan response only when you truly need extra bytes

3) Keep GATT tiny and explicit

GATT is your API surface. Make it stable and boring, like a good REST API.

  • One service for your product feature (plus standard services if needed)
  • Characteristics: Read for static values, Notify for live updates
  • Use little-endian and define units/scales (mV, °C×100, etc.)
  • Document a “version” characteristic for protocol compatibility

4) Optimize power with connection parameters

Most power wins come from how often the radio wakes up, not micro-optimizations.

  • Prefer longer connection intervals (start at 30–100 ms)
  • Use peripheral latency when you can (sleep through events)
  • Batch data into fewer notifications
  • Measure current on real hardware (don’t guess)
A reliable BLE device has a clear “story”

Advertising answers “who am I and why should you connect?” GATT answers “what can you do once connected?” Power is “how rarely can the radio wake up while still feeling responsive?”

Quick sanity test from a Linux laptop

If you can scan and see the expected name/service, you’re already ahead of many first prototypes. This is also a good way to confirm your advertising payload before spending time on an app.

# Linux BLE quick check (BlueZ)
# 1) Scan for nearby devices and check RSSI / advertising name
bluetoothctl
power on
scan on

# Watch for: Device XX:XX:XX:XX:XX:XX YourDeviceName
# Optional: narrow to a MAC once you see it
scan off
exit

# 2) More detailed view (if installed): scan and print advertising data
# sudo btmon can also be useful, but start simple.
sudo btmgmt --index 0 find
Don’t optimize too early

Get discovery + a minimal GATT service working first. Then measure power. The fastest way to lose time is tuning intervals while your payload or GATT behavior is still changing.

Overview

Bluetooth Low Energy is built around a simple workflow: devices advertise, a central (phone/gateway) decides to connect, and data moves through a structured attribute model called GATT. You can ship working BLE with a few lines of code—but shipping reliable BLE means understanding:

  • Advertising: how scanners discover you, filter you, and decide to connect.
  • GATT: how you expose data as a stable, versioned interface (services/characteristics).
  • Power: how advertising intervals + connection events dominate battery life.

What you’ll walk away with

Area What to decide Practical default
Advertising Connectable vs beacon, payload fields, interval Connectable, short name, one UUID, 250–1000 ms
GATT Services/characteristics, read vs notify, versioning One main service, read + notify, protocol version char
Power Connection interval, latency, batching, TX power 30–100 ms interval, use latency, batch notifications

Throughout this post, assume a common IoT setup: a low-power peripheral (sensor) connecting to a phone or gateway (central). The same principles apply to wearables, beacons, and embedded controllers—only the “best default” intervals and security choices vary.

Core concepts

This section builds the mental model you can use while debugging. BLE gets dramatically easier once you see which pieces live in advertising (pre-connection) and which live in GATT (post-connection).

Roles and the two phases of BLE

Roles

  • Peripheral: advertises; often low-power sensor/device
  • Central: scans; connects; often phone/gateway
  • Broadcaster/Observer: advertise/scan without connecting

Two phases

  • Advertising phase: discovery + lightweight metadata
  • Connection phase: structured data exchange via GATT/ATT

Advertising: discovery, identity, and “why connect?”

Advertising packets are short bursts on dedicated advertising channels. Scanners pick them up, apply filters, and only then decide to connect. Think of advertising as your device’s “business card”: it should be stable, small, and just informative enough to be discoverable and selectable.

Common advertising fields (and what they’re good for)

Field Best use Common mistake
Flags Basic capability bits; helps scanners interpret you Omitting them and “mysteriously” not showing up in some tools
Complete/Short Name Human-friendly discovery in phone lists Very long names that crowd out more useful data
Service UUIDs Machine filtering (“show only devices with X service”) Advertising many UUIDs “just in case”
Manufacturer/Service Data Small ID/version/battery snapshot; beacon payloads Streaming lots of changing data here at high rate (power drain)
Scan Response Optional extra bytes when the scanner asks for it Putting essential identity only in scan response (some scans won’t request it)
Connectable vs non-connectable

If you only need to broadcast a tiny payload (like an ID, occupancy bit, or coarse sensor value), a non-connectable beacon can be great. If you need configuration, history, firmware update, or richer data, you usually want a connectable peripheral with GATT.

GATT: your device’s API

GATT (Generic Attribute Profile) is how BLE models data once connected. Everything becomes an attribute with a handle. Services group related characteristics, and characteristics are the main “endpoints” you read, write, or subscribe to.

The building blocks

  • Service: a feature area (e.g., “Environmental Sensing”)
  • Characteristic: a value + properties (read/write/notify)
  • Descriptor: metadata (like CCCD for notifications)
  • UUID: identifier (standard 16-bit or custom 128-bit)

Read vs Notify (practical)

Reads are “pull” (central asks). Notifications are “push” (peripheral sends when enabled). For power and UX, notifications shine when updates are periodic or event-driven, and reads shine for configuration and infrequent status.

  • Read: device info, config, current snapshot
  • Notify: sensor stream, button presses, alerts

Power: what actually drains the battery

BLE power is dominated by radio activity. The CPU can usually sleep; the radio can’t. Your goal is to reduce how often the radio wakes up and how long it stays awake each time.

The three biggest levers

  • Advertising interval: how often you announce yourself when not connected
  • Connection interval: how often connection events happen when connected
  • Payload batching: sending fewer, bigger bursts instead of many small ones

Minimal GATT example (embedded mental model)

You don’t need to memorize any SDK. The point is to internalize: “service + characteristic + properties + callbacks”. Here’s a tiny example in a Zephyr-like style (common across modern BLE stacks).

/* Minimal custom service with a readable value and optional notifications (Zephyr-style) */
#include <zephyr/bluetooth/bluetooth.h>
#include <zephyr/bluetooth/gatt.h>

static uint16_t sensor_mv = 3300;

static ssize_t read_sensor(struct bt_conn *conn,
                           const struct bt_gatt_attr *attr,
                           void *buf, uint16_t len, uint16_t offset)
{
  return bt_gatt_attr_read(conn, attr, buf, len, offset, &sensor_mv, sizeof(sensor_mv));
}

/* Replace with your own 128-bit UUIDs */
#define BT_UUID_MY_SERVICE   BT_UUID_DECLARE_128(BT_UUID_128_ENCODE(0x12345678,0x1234,0x5678,0x1234,0x56789abcdef0))
#define BT_UUID_SENSOR_MV    BT_UUID_DECLARE_128(BT_UUID_128_ENCODE(0x12345679,0x1234,0x5678,0x1234,0x56789abcdef0))

BT_GATT_SERVICE_DEFINE(my_svc,
  BT_GATT_PRIMARY_SERVICE(BT_UUID_MY_SERVICE),
  BT_GATT_CHARACTERISTIC(BT_UUID_SENSOR_MV,
                         BT_GATT_CHRC_READ | BT_GATT_CHRC_NOTIFY,
                         BT_GATT_PERM_READ,
                         read_sensor, NULL, NULL),
);

static void start_adv(void)
{
  /* Keep advertising payload small and stable; add UUID so apps can filter quickly. */
  const struct bt_data ad[] = {
    BT_DATA_BYTES(BT_DATA_FLAGS, (BT_LE_AD_GENERAL | BT_LE_AD_NO_BREDR)),
    BT_DATA_BYTES(BT_DATA_UUID128_ALL, BT_UUID_128_ENCODE(0x12345678,0x1234,0x5678,0x1234,0x56789abcdef0)),
  };
  bt_le_adv_start(BT_LE_ADV_CONN_NAME, ad, ARRAY_SIZE(ad), NULL, 0);
}

Step-by-step

This is a practical build sequence you can apply to almost any BLE product: sensor, wearable, controller, beacon, or gateway accessory. Each step includes “what to do” and “what to watch out for” so you can avoid the common BLE potholes.

Step 1 — Define the connection story

Write your story as a state machine. It prevents accidental power drains and weird reconnection loops.

  • Not connected: advertise at interval X
  • Connected: exchange data at interval Y
  • Idle: use peripheral latency; only wake for events
  • Error: backoff and recover (don’t spam connect/disconnect)

Step 2 — Design advertising for discoverability and filtering

A scanner sees many devices. Your advertising should let the scanner quickly answer: “Is this my device?” and “Is it in the right mode?” without needing to connect.

A good advertising payload includes

  • Short device name (or a stable prefix)
  • One Service UUID (standard or custom)
  • A tiny “mode/version” byte in service/manufacturer data
  • Optional battery snapshot (if it helps UX)

Avoid these early on

  • Highly dynamic advertising data updated many times per second
  • Stuffing everything into scan response and assuming scanners will request it
  • Very fast advertising intervals without a reason
  • Multiple “almost identical” product variants with no versioning
Version your protocol from day one

Add a protocol version characteristic (and optionally a version byte in advertising/service data). It’s a tiny addition that saves hours when you change formats later.

Step 3 — Build a GATT “contract” (services + characteristics)

Think of GATT as a contract between your device firmware and every client (phone app, gateway, test tool). Keep it stable. Add new characteristics rather than breaking old ones.

A practical starter GATT layout

Characteristic Properties Notes
Protocol version Read Single byte or uint16; never changes for a given firmware
Live sensor value Read + Notify Read gives a snapshot; Notify streams updates when enabled
Device config Read + Write Prefer structured binary (fixed fields) or small TLV
Command/control Write (optionally Write Without Response) Keep commands idempotent; return status via a notify characteristic
Notifications don’t “just happen”

The central must enable notifications (via CCCD). Your peripheral must handle: “CCCD enabled → start sending” and “disabled → stop sending” cleanly, or you’ll waste power and confuse clients.

Step 4 — Pick connection parameters that balance UX and power

Connection parameters decide how often both devices wake up for connection events. Too fast wastes power; too slow can feel laggy. The “right” default depends on your data rate and UX expectations.

Starter defaults (good for many sensors)

  • Connection interval: 30–100 ms
  • Peripheral latency: 0–10 (sleep through events when idle)
  • Supervision timeout: 4–6 s (enough for noisy environments)
  • Notifications: batch to 1–10 Hz unless you truly need more

When to go faster

  • Human interaction (button press, UI feels laggy)
  • Real-time control loops (rare in BLE; be careful)
  • Short burst transfers (send quickly, then return to slow)
  • Audio-like streaming (usually not BLE’s sweet spot)

Step 5 — Test with a real client (and log everything)

BLE failures often come from mismatched assumptions between client and device: MTU sizes, notification enable/disable, reconnect behavior, or background constraints on phones. Test early with at least two different clients (e.g., a phone app + a script).

This example uses Python + Bleak to scan, connect, read a characteristic, and subscribe to notifications. It’s an excellent “known-good” harness when debugging embedded firmware.

# pip install bleak
import asyncio
from bleak import BleakScanner, BleakClient

TARGET_NAME = "YourDeviceName"  # or filter by service UUID if you prefer
SVC_UUID = "12345678-1234-5678-1234-56789abcdef0"
CHAR_UUID = "12345679-1234-5678-1234-56789abcdef0"

def on_notify(_, data: bytearray):
    # Example: uint16 little-endian millivolts
    mv = int.from_bytes(data[:2], byteorder="little", signed=False)
    print(f"notify: sensor_mv={mv}")

async def main():
    devices = await BleakScanner.discover(timeout=5.0)
    target = next((d for d in devices if d.name == TARGET_NAME), None)
    if not target:
        raise SystemExit("Target not found. Check advertising name/interval and distance.")

    async with BleakClient(target) as client:
        # Optional: ensure expected service exists
        svcs = await client.get_services()
        if SVC_UUID.lower() not in [s.uuid.lower() for s in svcs]:
            print("Warning: expected service UUID not found. GATT may differ from your assumptions.")

        value = await client.read_gatt_char(CHAR_UUID)
        mv = int.from_bytes(value[:2], "little")
        print(f"read: sensor_mv={mv}")

        await client.start_notify(CHAR_UUID, on_notify)
        await asyncio.sleep(10)  # keep alive to receive notifications
        await client.stop_notify(CHAR_UUID)

asyncio.run(main())
Debugging order that saves time
  • Confirm advertising visibility (name + UUID)
  • Confirm connection stability (no rapid disconnects)
  • Confirm GATT shape (services/characteristics match expectations)
  • Only then tune intervals and notifications for power

Step 6 — Measure power on-device (and iterate)

If battery life matters, measure current on real hardware. Look for three patterns: (1) advertising current while disconnected, (2) connection event spikes while idle, and (3) sustained radio activity while streaming.

Fast power wins

  • Increase advertising interval after first pairing
  • Use slow connection parameters when idle
  • Send fewer notifications (batching)
  • Lower TX power if range allows

Symptoms and likely causes

  • High idle current: too-fast connection interval, no latency, frequent notifications
  • Spiky, frequent wakes: scanning continuously on the device, chatty logs, timers
  • Short battery life only when connected: streaming design, retries, poor RF

Common mistakes

BLE bugs often look like “random radio issues”, but the root cause is usually a design assumption. Here are the most common pitfalls and the fixes that work in practice.

Mistake 1 — Advertising too fast “to be safe”

Fast advertising improves discovery, but it can quietly dominate power budget.

  • Fix: start with 250–1000 ms and only go faster when UX demands it.
  • Fix: use fast advertising briefly (e.g., 20–30 s) then fall back to slower interval.

Mistake 2 — Putting essential identity only in scan response

Not all scanners request scan response (or do so reliably in the background).

  • Fix: keep the most important filter (service UUID or product ID) in the primary advertisement.
  • Fix: treat scan response as “nice-to-have extra bytes”.

Mistake 3 — A GATT layout that changes every firmware

Clients break when handles/meaning shift. Versioning avoids pain.

  • Fix: add a protocol version characteristic and keep backwards compatibility.
  • Fix: add new characteristics instead of changing existing ones.

Mistake 4 — Notifications without CCCD state handling

If you ignore enable/disable, you waste power and confuse apps.

  • Fix: start sending only when notifications are enabled; stop immediately when disabled.
  • Fix: on reconnect, assume “disabled” until explicitly enabled again.

Mistake 5 — Ultra-fast connection interval for a slow data stream

A 7.5 ms interval looks “high performance” but often just burns battery.

  • Fix: choose interval based on update needs (e.g., 1–10 Hz data rarely needs very fast intervals).
  • Fix: use “burst fast → return slow” for transfers/config.

Mistake 6 — No reconnect/backoff strategy

Rapid connect/disconnect loops drain batteries on both sides and look like RF problems.

  • Fix: implement exponential backoff on failures.
  • Fix: log disconnect reasons and treat timeouts differently than user-initiated disconnects.
Phone background behavior is not “just BLE”

Mobile OSes may throttle scans, delay connections, or limit background callbacks. When a device “works in the foreground but not in the background,” validate your advertising identity and app permissions first, then test with a second client (like a laptop script) to isolate device vs app behavior.

FAQ

What’s the difference between BLE and “classic” Bluetooth?

BLE is optimized for low power and small, intermittent data (sensors, wearables, beacons). Classic Bluetooth is built for sustained throughput (audio, continuous streaming). BLE’s key idea is short radio bursts plus sleep in-between—so the design focus shifts to advertising and connection parameters.

How big is a BLE advertising payload?

The usable space is small and shared by multiple fields (name, UUIDs, service/manufacturer data). That’s why good advertising focuses on identity + filtering, not full data. If you need richer payloads, connect and use GATT.

Should I put sensor data in advertisements or in GATT?

If you need only a tiny broadcast (ID + a few bytes) and don’t need configuration, beacon-style advertising can be great. If you need reliability, history, configuration, encryption, or multi-value data, use GATT. A common hybrid is: advertise an ID + mode; connect for details.

Notifications vs indications: which should I use?

Notifications are faster and lighter—best for most streaming updates. Indications add acknowledgment (reliability) but cost more airtime and may reduce throughput. If “every update must arrive” truly matters, consider indications or an application-level ack protocol, but measure power impact.

Why can’t my phone see my BLE device?

In practice, the top causes are: advertising not started (or stopped), advertising interval extremely slow, weak RF (range), or the device identity not matching your scan filters (UUID/name mismatch). Also verify your device isn’t connecting to a different central and stopping advertising (many peripherals advertise only when not connected).

How do I choose an advertising interval?

Choose interval based on UX: faster = quicker discovery, slower = better battery. A solid starting point for many products is 250–1000 ms, with a “fast burst” right after boot/pairing (e.g., ~100–200 ms for 20–30 seconds), then fall back to a slower interval.

Do I need pairing/bonding for a simple sensor?

Not always. If data is not sensitive and configuration changes are harmless, you can skip pairing for simplicity. If you’re exposing personal data, controlling actuators, or doing firmware updates, pairing/encryption is usually worth it. Decide early because security changes can affect client behavior and reconnection flows.

Cheatsheet

A scan-fast checklist for BLE advertising, GATT design, and power settings.

Advertising checklist

  • Keep payload small: name + one UUID + small ID/version
  • Start with 250–1000 ms interval
  • Use scan response only for extra, non-essential bytes
  • Prefer stable fields (avoid frequently changing adv data)
  • Plan for “fast burst then slow” strategy

GATT checklist

  • One main service for your product feature
  • Protocol version characteristic (read-only)
  • Read for snapshot/config; Notify for updates
  • Define units and scaling (document endianness)
  • Handle CCCD enable/disable (notifications)

Power checklist

  • Use longer connection intervals when idle (30–100 ms to start)
  • Use peripheral latency to sleep through events
  • Batch notifications (fewer radio wakes)
  • Lower TX power if range allows
  • Measure current on hardware after functionality is stable

Debug checklist

  • Verify advertising identity (name/UUID) with two tools
  • Confirm stable connect/disconnect behavior
  • List GATT services and verify characteristic properties
  • Test notifications: enable → receive → disable → stop
  • Test across at least 2 phones or phone + laptop

Suggested “starter defaults” snapshot

Setting Default Adjust when…
Advertising interval 250–1000 ms Need faster discovery (go faster) or longer battery (go slower)
Connection interval 30–100 ms UI feels laggy (go faster) or data is infrequent (go slower)
Peripheral latency 0–10 Idle periods exist (increase) vs frequent interactive actions (decrease)
Notification rate 1–10 Hz High-rate streaming required (increase carefully, measure power)

Wrap-up

BLE is easiest when you treat it like a product interface: keep advertising small and filterable, keep GATT stable and versioned, and use connection parameters to keep the radio asleep as much as possible. Start with reliable discovery and a minimal GATT “contract”, then tune power with measurements—not guesses.

Next actions (15 minutes)
  • Scan your device and confirm the advertising name + UUID are correct
  • Write down your GATT contract (service + 3–6 characteristics)
  • Pick one interval default and measure current once features are stable
  • Bookmark the Cheatsheet and use it for every new BLE prototype

If you’re building a full IoT device, pair this with power budgeting, OTA updates, and a reliable messaging backbone (like MQTT). Those pieces turn a BLE prototype into a shippable product.

Quiz

Quick self-check (demo). This quiz is auto-generated for hardware / iot / embedded.

1) In BLE, what is the main purpose of advertising?
2) Which statement best describes GATT?
3) What is usually the biggest driver of BLE power consumption?
4) What’s the key practical difference between Read and Notify in GATT?