How to Design BLE Advertising Payloads

You have 254 bytes. That’s it. After mandatory overhead, you’re often left with fewer than 200 bytes to describe everything your device needs to broadcast to the world: its identity, its state, its sensor readings, its health. Most engineers waste half of those bytes on their first attempt because they design their payload like they’re working with a modern API instead of a protocol that counts every bit.
Those 254 bytes in BLE 5.0+ extended advertising represent a massive upgrade from the 31 bytes legacy advertising allowed. Yet most design guidance still treats payloads as an afterthought, something you figure out after the hardware is finalized. That’s backwards. Your payload design directly determines your battery life, your scanner compatibility, and whether you’ll need a painful firmware update six months after deployment.
This guide walks through designing BLE advertising payloads from first principles, using an asset tracker as a running example. You should be comfortable with BLE basics and byte-level data representation. By the end, you’ll have a complete, working payload structure and the principles to design your own.
The BLE Advertising Format: AD Structures All the Way Down
Every BLE advertising payload is a sequence of AD (Advertising Data) structures. Each structure follows an identical format:
+----------+----------+------------------+
| Length | AD Type | AD Data |
| (1 byte) | (1 byte) | (Length-1 bytes) |
+----------+----------+------------------+The Length field specifies how many bytes follow (including the AD Type byte). The AD Type identifies what kind of data this structure contains: flags, device name, service UUIDs, or manufacturer-specific data. The Bluetooth SIG maintains the authoritative list of AD Type codes.
Here’s what this looks like in practice:
// Example: Parsing AD structures from raw advertising data
void parse_ad_structures(uint8_t *adv_data, uint8_t adv_len) {
uint8_t offset = 0;
while (offset < adv_len) {
uint8_t length = adv_data[offset];
if (length == 0) break; // End of structures
uint8_t ad_type = adv_data[offset + 1];
uint8_t *ad_data = &adv_data[offset + 2];
uint8_t data_len = length - 1;
// Process based on ad_type...
offset += length + 1; // Move to next structure
}
}Legacy vs. Extended Advertising:
| Attribute | Legacy (BLE 4.x) | Extended (BLE 5.0+) |
|---|---|---|
| Max payload | 31 bytes | 254 bytes |
| Scan response | Separate 31 bytes | Included in chain |
| Advertising sets | 1 | Multiple (vendor-dependent) |
| Data rate options | 1 Mbps only | 1 Mbps, 2 Mbps, Coded PHY |
The Flags AD Type (0x01) is effectively mandatory for connectable advertising and consumes 3 bytes. A short device name easily takes 5-10 bytes. On legacy advertising, you might have only 18 bytes left for actual beacon data. Extended advertising gives you breathing room, but the principles of efficient design still matter. Every byte transmitted costs power.
Designing Your Data Schema
Start with requirements, not bytes. What information must your device broadcast? For our asset tracker example:
- Device identifier: Distinguish this tracker from others
- Battery level: Enable low-battery alerts without connecting
- Motion state: Currently moving, stationary, or unknown
- Temperature reading: Environmental monitoring (optional)
- Payload version: Future-proofing
Then translate requirements into fields:
| Field | Size | Encoding | Notes |
|---|---|---|---|
| Version | 4 bits | Unsigned | Payload format version (0-15) |
| Motion State | 2 bits | Enum | 0=unknown, 1=stationary, 2=moving, 3=reserved |
| Battery Level | 6 bits | Unsigned | Percentage (0-63 maps to 0-100%) |
| Device ID | 4 bytes | Unsigned | Unique identifier |
| Temperature | 2 bytes | Signed, 0.01°C | -327.68°C to +327.67°C range |
| Total | 7 bytes |
Notice the deliberate choices here. Motion state needs only 3 values, so 2 bits suffice. Battery percentage doesn’t need 1% precision. Mapping 0-63 to 0-100% gives ~1.6% resolution, plenty for “low battery” decisions. The version field uses 4 bits because you won’t need 256 payload versions, but you might need 15.
// Payload structure definition
typedef struct __attribute__((packed)) {
uint8_t version_motion_battery; // 4 bits version | 2 bits motion | 6 bits battery (crosses byte boundary - see encoding)
uint32_t device_id; // Little-endian
int16_t temperature; // 0.01°C units, little-endian
} asset_tracker_payload_t;Fixed vs. Variable-Length Fields:
Fixed-length fields parse faster and produce predictable payload sizes. Variable-length fields (like strings) offer flexibility but complicate parsing and make size budgeting harder. For advertising payloads, prefer fixed-length unless you have a compelling reason otherwise. If you need variable data, use a length prefix or TLV encoding within your manufacturer data.
Endianness: BLE is little-endian. Your payload should be too. If your MCU is big-endian (rare these days), convert before transmission.
Encoding Strategies for Efficiency
Bit-packing is your primary tool. Instead of burning a full byte for a boolean, combine multiple small values:
// Encoding: Pack version (4 bits), motion (2 bits), battery (6 bits) into 12 bits
// Stored across 2 bytes for byte alignment
uint8_t encode_status_byte1(uint8_t version, uint8_t motion, uint8_t battery_pct) {
// Convert battery percentage (0-100) to 6-bit value (0-63)
uint8_t battery_6bit = (battery_pct * 63) / 100;
// Byte 1: version (4 bits) | motion (2 bits) | battery high 2 bits
return (version << 4) | (motion << 2) | (battery_6bit >> 4);
}
uint8_t encode_status_byte2(uint8_t battery_pct) {
uint8_t battery_6bit = (battery_pct * 63) / 100;
// Byte 2: battery low 4 bits | 4 reserved bits (set to 0)
return (battery_6bit & 0x0F) << 4;
}Scaled Integers vs. Floats:
Never use floats in advertising payloads. A float consumes 4 bytes and introduces parsing complexity. Instead, use scaled integers:
- Temperature: int16 in 0.01°C units (2 bytes covers -327°C to +327°C)
- Humidity: uint8 in 0.5% units (1 byte covers 0-127.5%)
- Voltage: uint16 in millivolts (2 bytes covers 0-65.535V)
Reserved Bits: Always leave a few bits reserved for future use. Set them to zero. This costs nothing today and provides expansion room tomorrow. Our example has 4 reserved bits in the second status byte.
Compression: Don’t bother. At payload scales (under 254 bytes), compression overhead typically exceeds savings. The exception is if you’re transmitting highly repetitive data, which is rare in advertising use cases.
Versioning and Extensibility
Your payload will change. Devices in the field will run old firmware while new devices broadcast updated formats. Handle this now or suffer later.
Simple Strategy: Reserve the first 4 bits for a version number. Parsers check the version and use the appropriate decoding logic:
void decode_payload(uint8_t *data, uint8_t len) {
uint8_t version = data[0] >> 4;
switch (version) {
case 1:
decode_v1_payload(data, len);
break;
case 2:
decode_v2_payload(data, len); // Added humidity field
break;
default:
// Unknown version - log and skip, don't crash
break;
}
}Forward-Compatibility Principle: Parsers must ignore fields they don’t understand. If version 2 adds a humidity byte at the end, version 1 parsers should simply not read it, not reject the entire payload.
Resist the urge to over-engineer. Full TLV (Type-Length-Value) encoding within your manufacturer data adds flexibility but costs bytes and complexity. For most advertising payloads, a version byte plus fixed-layout evolution works fine.
Platform Constraints: Spec vs. Reality
The Bluetooth specification allows 254 bytes. Your SDK might not. Common real-world constraints:
- Nordic nRF5 SDK: Extended advertising payload max varies by SoftDevice version
- ESP-IDF: Historically limited extended advertising payload size
- Multiple advertising sets: Spec allows many; silicon may support fewer
- Advertising interval minimums: Affect power consumption calculations
Recommendation: Before finalizing your payload design, write a test that constructs your maximum-size payload and transmits it on your target hardware. Discovering a 200-byte SDK limit after designing a 230-byte payload is painful.
Putting It Together: Complete Payload Example
Here’s the full asset tracker payload, wrapped in the required AD structures:
#define COMPANY_ID_EXAMPLE 0xFFFF // Use your registered ID in production
#define AD_TYPE_FLAGS 0x01
#define AD_TYPE_MFG_DATA 0xFF
#define PAYLOAD_VERSION 1
void build_advertising_payload(uint8_t *buffer, uint8_t *len,
uint32_t device_id,
uint8_t battery_pct,
uint8_t motion_state,
int16_t temp_centidegrees) {
uint8_t pos = 0;
// AD Structure 1: Flags (mandatory for discoverability)
buffer[pos++] = 0x02; // Length
buffer[pos++] = AD_TYPE_FLAGS; // AD Type
buffer[pos++] = 0x06; // Flags: LE General Discoverable, BR/EDR not supported
// AD Structure 2: Manufacturer Specific Data
buffer[pos++] = 0x0A; // Length: 1 (type) + 2 (company ID) + 7 (our data)
buffer[pos++] = AD_TYPE_MFG_DATA;
// Company ID (little-endian)
buffer[pos++] = COMPANY_ID_EXAMPLE & 0xFF;
buffer[pos++] = (COMPANY_ID_EXAMPLE >> 8) & 0xFF;
// Our custom payload
buffer[pos++] = encode_status_byte1(PAYLOAD_VERSION, motion_state, battery_pct);
buffer[pos++] = encode_status_byte2(battery_pct);
// Device ID (little-endian)
buffer[pos++] = device_id & 0xFF;
buffer[pos++] = (device_id >> 8) & 0xFF;
buffer[pos++] = (device_id >> 16) & 0xFF;
buffer[pos++] = (device_id >> 24) & 0xFF;
// Temperature (little-endian)
buffer[pos++] = temp_centidegrees & 0xFF;
buffer[pos++] = (temp_centidegrees >> 8) & 0xFF;
*len = pos;
}Final Payload Map:
| Offset | Field | Example Value | Bytes |
|---|---|---|---|
| 0 | Flags Length | 0x02 | 02 |
| 1 | Flags AD Type | 0x01 | 01 |
| 2 | Flags Data | 0x06 | 06 |
| 3 | Mfg Data Length | 0x0A | 0A |
| 4 | Mfg Data AD Type | 0xFF | FF |
| 5-6 | Company ID | 0xFFFF | FF FF |
| 7 | Version/Motion/Battery (high) | v1, moving, 75% | 16 |
| 8 | Battery (low) + Reserved | B0 | |
| 9-12 | Device ID | 0x12345678 | 78 56 34 12 |
| 13-14 | Temperature | 23.45°C = 2345 | 29 09 |
Total: 15 bytes, leaving 239 bytes available for additional data in extended advertising.
Mistakes That Will Cost You Later
- Forgetting mandatory AD Types: Those 3 bytes for Flags aren’t optional for discoverable devices. Budget for them.
- Using 0xFFFF as your Company ID in production: That’s reserved for testing. Register with the Bluetooth SIG or use a partner’s ID with permission.
- Ignoring endianness: Multi-byte values must be little-endian. This will silently corrupt your data on big-endian platforms.
- Designing for 254 bytes without testing: Verify your SDK’s actual limit before committing to a payload size.
- No version field: You will update this payload. Make parsing version-aware from day one.
Making Your Payload Production-Ready
Start with these principles: understand the AD structure format, pack your data efficiently, version from the beginning, and verify against real hardware early. Design for your actual requirements. If 7 bytes of beacon data is enough, don’t use 70 just because you can.
Build a test scanner that parses your payload and displays every field. Use it throughout development. When you inevitably need to add a field six months from now, you’ll be grateful you left those reserved bits and included that version nibble.
Hubble Network extends your BLE devices to satellite connectivity—no hardware changes required. See how it works →