top of page

BLE GATT Profile Design Explained: Services, Characteristics, and the Power Budget Trade-offs Engineers Get Wrong

  • Writer: Srihari Maddula
    Srihari Maddula
  • 11 minutes ago
  • 7 min read

Srihari Maddula • Founder & Technical Lead, Eurth Techtronics Pvt Ltd 

Category: Firmware Architecture & RTOS 

Estimated Reading Time: 9 min


A team building their first custom BLE product almost always makes the same early mistake: designing the GATT profile as a data model first — what fields does the device need to expose — and treating the power and timing consequences of that model as an afterthought to optimize later. By the time "later" arrives, the profile is baked into a mobile app's integration code and a certification submission, and the actual fix (restructuring which data lives in which characteristic, and how often each gets touched) is a much bigger change than it would have been at the design stage. This post covers GATT profile design as what it actually is: a power and timing architecture decision wearing a data-modeling costume.



Overview: What GATT Actually Governs


The Generic Attribute Profile (GATT) defines how a BLE peripheral exposes data to a central device (typically a phone): as a hierarchy of Services, each containing Characteristics, each of which can be read, written, or subscribed to for notifications. This structure is not just an API surface — every characteristic read, write, and especially every notification has a direct radio-airtime cost, and radio airtime is, for a battery-powered peripheral, the single most expensive thing the device does. A GATT profile designed purely around what data the app needs, without regard for how often each characteristic will actually be touched and by what mechanism, routinely produces a working integration with meaningfully worse battery life than a profile designed with the traffic pattern in view from the start.


Technical Details & Specifications: Services, Characteristics, and Their Real Costs


A minimal, well-structured custom profile — using the same real device from our BLE wearable work as the reference — defines one custom service with a handful of purpose-specific characteristics, each with deliberately chosen properties (read, write, notify) rather than defaulting every characteristic to support all three.


// GATT service and characteristic UUIDs -- 16-bit custom UUIDs

// used here for brevity in this internal service range; a real

// product should use a full 128-bit UUID to avoid collision with

// other vendors' custom services

#define SVC_UUID            0xFFF0   // Custom adherence-monitor service

#define CHAR_EVENT_DATA_UUID 0xFFF1  // Notify only -- new gesture events

#define CHAR_STATUS_UUID     0xFFF2  // Read + notify -- battery, device state

#define CHAR_CONFIG_UUID     0xFFF3  // Read + write -- detection thresholds

#define CHAR_CRASHLOG_UUID   0xFFF4  // Read only -- diagnostic data, rarely accessed

 

// Characteristic properties matter as much as their data content --

// each property maps to a different traffic pattern and power cost

static const att_db_desc_t char_table[] = {

    { CHAR_EVENT_DATA_UUID, ATT_PROP_NOTIFY,            12 },  // 12-byte event struct

    { CHAR_STATUS_UUID,     ATT_PROP_READ | ATT_PROP_NOTIFY, 4 },

    { CHAR_CONFIG_UUID,     ATT_PROP_READ | ATT_PROP_WRITE,  8 },

    { CHAR_CRASHLOG_UUID,   ATT_PROP_READ,               64 },  // large, but rarely read

};

GATT operation

Radio cost

Typical use pattern

Read (central-initiated)

One request/response exchange, on demand

Config values, occasional status checks

Write (central-initiated)

One request/response exchange, on demand

Config changes, commands

Notify (peripheral-initiated)

Sent proactively whenever the value changes — cost scales with change frequency, not app polling

Event streams, frequently-changing status

Indicate

Like notify, but with mandatory acknowledgment — higher reliability, higher airtime cost per update

Critical data where delivery confirmation matters more than airtime cost

The distinction between notify and a polled read is the single highest-leverage GATT design decision for battery life. A characteristic the app reads by polling every few seconds costs radio airtime on every poll regardless of whether the value actually changed. A characteristic set up for notification only sends data when the value genuinely changes, and — critically — the connection can run at a much longer connection interval when nothing needs to be sent, waking only when there's an actual notification to deliver. A GATT design that defaults every characteristic to "read + notify, app decides how to use it" pushes a power-relevant decision onto the app team, who frequently don't have visibility into the firmware-side power cost of their polling choice.


THE RULE:  Every characteristic set to support polling as well as notification is an invitation for the app team to poll, because polling is simpler to implement than subscription logic — and polling is almost always the more power-expensive choice for the firmware side. Design the properties to make the power-cheap choice the only easy one.


Advantages: What a Deliberately-Scoped Profile Buys You


A minimal, purpose-scoped profile — few characteristics, each with tightly-matched properties to its actual traffic pattern — has three concrete advantages over a broad, permissive one. First, and most directly, lower average radio-on time, because notify-only characteristics only transmit on genuine state change rather than on app-driven polling cadence. Second, a smaller attack surface for BLE security review — every readable/writable characteristic is a potential point of unauthorized access if pairing/bonding isn't correctly enforced, and a profile with fewer, more tightly-scoped characteristics is genuinely easier to audit and get right. Third, and easy to underweight during initial design: a smaller, well-documented profile is meaningfully easier for a third-party app developer (or a future engineer on your own team) to integrate against correctly, because there's less ambiguity about which characteristic to use for which purpose.


Challenges & Trade-offs: Where Minimal Profiles Cause Their Own Problems


A profile that's too minimal creates its own failure mode: if event data, status, and configuration are all crammed into too few characteristics to save on GATT table overhead, the app side ends up parsing multi-purpose payloads with internal type tags — effectively reinventing a sub-protocol inside a single characteristic, which pushes complexity into application-layer parsing logic that GATT's structure was meant to avoid. There's a real balance point between "too many characteristics, each touched inefficiently" and "too few characteristics, each overloaded with mixed-purpose data," and it's found by grouping data specifically by update frequency and consumer, not by data type alone — two pieces of data that change at wildly different rates don't belong in the same characteristic even if they're conceptually related, because the higher-frequency field will drag unnecessary notification traffic for the lower-frequency one bundled alongside it.


Connection parameter negotiation is the other genuine challenge: the peripheral can request a preferred connection interval, but the central device (the phone's OS) has final say, and phone OS BLE stacks (iOS and Android both) apply their own policies and constraints that can override a peripheral's power-optimal request — an interval that tested well against a specific Android version's BLE stack in the lab can behave differently against iOS, or against a different Android OEM's modified BLE stack in the field. This is a genuinely hard-to-fully-control variable, and the practical mitigation is testing against the actual range of target devices early, not assuming a single reference phone's behavior generalizes to the full target device population.


THE RULE:  The phone's OS, not your firmware, has final authority over the actual connection interval used. Design for a wide acceptable range and test against real target-device diversity — a single reference phone's behavior does not generalize.


Case Study: Restructuring a Profile Mid-Development


An early draft of the profile referenced above put battery percentage, device state, and firmware version into a single "status" characteristic that the app polled every 30 seconds — a reasonable-looking design choice that treated all three as "device status information," conceptually related and therefore, it seemed, naturally grouped. In practice, firmware version essentially never changes after initial pairing, device state changes only on specific events (charging start/stop, error conditions), and battery percentage changes gradually but continuously. Polling all three together every 30 seconds meant paying full radio-airtime cost for firmware version and device state on every single poll, for data that was, the overwhelming majority of the time, identical to the previous read.


The fix, made during a mid-development profile revision (costly enough to require app-side integration changes, but far cheaper than a post-certification change would have been), split this into a notify-only battery characteristic (updates only on meaningful percentage change, not on a timer) and a separate, rarely-touched device-info characteristic covering firmware version and static identifiers, read once at connection time rather than polled. Device state moved to notify-only as well, firing only on actual state transitions. The measured result was a meaningful reduction in average connected-mode radio-on time, without any change to what data the app actually had access to — purely a restructuring of how and when that data moved across the radio link. The broader lesson: grouping GATT data by conceptual category rather than by update frequency is an intuitive mistake, and it's specifically the kind of mistake that doesn't show up as a bug — the profile works correctly, it's just quietly more expensive than it needs to be, which is exactly why it's easy to ship without noticing.


Implementation Plan: Designing a GATT Profile Deliberately


  • Before defining any characteristic, list every piece of data the device needs to expose along with its actual update frequency — continuous, event-driven, or effectively static — and group characteristics by that frequency, not by conceptual category.

  • For every characteristic, choose properties (read/write/notify/indicate) that match its actual access pattern — don't default to enabling everything 'in case the app needs it'; that default pushes power-relevant decisions onto app developers who can't see the firmware-side cost.

  • Keep payload sizes tight and purpose-built per characteristic — a compact, well-defined struct (as in the event-data example above) beats a generic, larger payload with unused fields, since every notification's airtime cost scales with payload size.

  • Design connection parameter requests for a realistic acceptable range, and test against actual target-device diversity (multiple Android OEMs, multiple iOS versions) rather than a single reference device, since the OS ultimately controls the negotiated interval.

  • Document the profile's intended traffic pattern alongside its UUID table — which characteristics are meant to be polled occasionally versus subscribed to continuously — so a future app integration doesn't accidentally reintroduce a polling pattern the profile was specifically designed to avoid.

  • Revisit the profile explicitly once real usage telemetry exists — actual connected-mode radio-on time measured in the field is the ground truth a design-stage estimate can't fully replace, and a profile that looked well-structured on paper may still reveal an unexpected hot characteristic once real traffic patterns are visible.


Conclusion: GATT Design Is Power Architecture, Not Data Modeling


The recurring theme across every section here is the same one: a GATT profile's structure isn't a neutral data-modeling exercise that happens to run over a radio — it's a direct, load-bearing determinant of the device's power consumption and connection behavior, and treating it as an afterthought to a data model designed first is how a working BLE integration ends up quietly more power-hungry than it needed to be. The teams that get this right design the profile with update frequency and traffic pattern as first-class inputs from the start, choose characteristic properties deliberately rather than permissively, and revisit the design against real field telemetry rather than assuming the design-stage estimate was the final word — the same discipline that shows up everywhere else in battery-powered embedded design, applied specifically to the one layer that's easiest to treat as "just an API" until the battery-life numbers come back worse than expected.


EurthTech delivers AI-powered embedded systems, IoT product engineering, and smart infrastructure solutions — Hyderabad, India. www.eurthtech.com

 
 
 

Comments


EurthTech delivers AI-powered embedded systems, IoT product engineering, and smart infrastructure solutions to transform cities, enterprises, and industries with innovation and precision.

Factory:

Plot No: 41,
ALEAP Industrial Estate, Suramapalli,
Vijayawada,

India - 521212.

  • Linkedin
  • Twitter
  • Youtube
  • Facebook
  • Instagram

 

© 2025 by Eurth Techtronics Pvt Ltd.

 

Development Center:

4th Floor, Krishna towers, 100 Feet Rd, Madhapur, Hyderabad, Telangana 500081

Menu

|

Accesibility Statement

bottom of page