Beacon Advertising Packet Design & Payload Optimization

Every Bluetooth beacon lives or dies by its advertising packet. The packet is the beacon’s voice — a 31-byte whisper on legacy BLE, or a 255-byte shout on extended advertising. How you pack those bytes determines whether a receiver detects your beacon in 50 ms or 500 ms, whether the battery lasts 18 months or 3 years, and whether 200 beacons in the same room produce a clean signal or a collision-ridden mess.

The Legacy 31-Byte Advertising Packet

A legacy BLE advertising packet (BLE 4.x) has exactly 31 bytes of payload space, split into AD (Advertising Data) structures. Each AD structure consumes 2 bytes of overhead (length + type), leaving 29 bytes for actual data — but only if you use a single AD structure. In practice, you need at least two:

AD Structure Type Purpose Bytes Used
Flags 0x01 LE General Discoverable, BR/EDR Not Supported 3 (2 header + 1 data)
Manufacturer Specific 0xFF Company ID + custom payload 2 + 2 + N
Complete Local Name 0x09 Device name string 2 + name length
TX Power Level 0x0A Signed int8, -127 to +127 dBm 3
Service UUID (128-bit) 0x07 Complete list of 128-bit UUIDs 18

After Flags (3 bytes) and the mandatory Manufacturer Specific header (4 bytes for type+length+company ID), you have 24 bytes for your actual beacon payload. That’s the budget for UUID, major, minor, TX power, and any sensor data. The iBeacon format uses all 24 bytes for UUID (16) + Major (2) + Minor (2) + TX Power (1) + reserved (3), leaving zero room for sensor data.

Frame Format Comparison

Three dominant frame formats compete for those 24 bytes. Each makes different tradeoffs:

Format Company ID Payload Layout Sensor Data? Ecosystem Lock-in
iBeacon 0x004C (Apple) UUID(16) + Major(2) + Minor(2) + TX(1) No — fixed 21 bytes Apple ecosystem only
Eddystone-UID 0x00AA (Google) Namespace(10) + Instance(6) + TX(1) No — fixed 17 bytes Google Nearby (deprecated)
Eddystone-TLM 0x00AA (Google) Version(1) + Batt(2) + Temp(2) + AdvCnt(4) + SecCnt(4) Yes — battery & temp Google ecosystem
Custom (Open) 0xXXXX (assigned) Any layout — you define the schema Yes — flexible None — your own receiver

The practical recommendation for most deployments: use a custom frame format. You gain full control of the 24 bytes, can embed sensor data directly in the advertising packet (avoiding the need for a GATT connection), and aren’t locked into an ecosystem that may be deprecated (as Google did with Nearby in 2021).

Designing a Custom Payload: Squeezing Sensors into 24 Bytes

A well-designed custom payload can carry identity, telemetry, and flags in a single 24-byte window — enough to eliminate GATT connections entirely for many use cases:

Field Offset Size Range / Resolution Description
Frame Version 0 1 byte 0–255 Protocol version for forward compatibility
Device ID 1 6 bytes 14 trillion IDs Unique device identifier (MAC-derived)
Battery Level 7 1 byte 0–100% (1% res) Unsigned int, 0xFF = not available
Temperature 8 1 byte -40 to +85°C (0.5°C) Int8, raw = temp × 2 + 40
Humidity 9 1 byte 0–100% (0.4% res) Unsigned, raw = humidity × 2.55
Accelerometer 10 3 bytes ±16g (0.125g) 3 × int8, packed XYZ
Button / Event 13 1 byte 0–255 events Counter for button press / tamper
Tamper Flag 14 1 byte 0 = OK, 1 = tampered Physical tamper status
TX Power 15 1 byte -127 to +127 dBm Signed int8 for RSSI@1m calibration
Sequence Counter 16 4 bytes 0–4 billion Anti-replay, uptime estimate
Reserved 20 4 bytes Future use / CRC / extension

Encoding tricks for squeezing more data:

// Temperature: -40 to +85°C in 1 byte (0.5°C resolution)
uint8_t encode_temp(float temp_c) {
    int16_t raw = (int16_t)((temp_c + 40.0) * 2.0);  // offset + scale
    if (raw < 0) raw = 0;
    if (raw > 250) raw = 250;  // cap at +85°C
    return (uint8_t)raw;
}
float decode_temp(uint8_t raw) {
    return (raw / 2.0) - 40.0;
}

// Humidity: 0-100% in 1 byte (0.4% resolution)
uint8_t encode_humidity(float rh) {
    return (uint8_t)(rh * 2.55);
}
float decode_humidity(uint8_t raw) {
    return raw / 2.55;
}

// Accelerometer: ±16g in 3 bytes (0.125g per LSB)
void encode_accel(float x_g, float y_g, float z_g, uint8_t out[3]) {
    out[0] = (uint8_t)(x_g / 0.125 + 128);  // center at 128 = 0g
    out[1] = (uint8_t)(y_g / 0.125 + 128);
    out[2] = (uint8_t)(z_g / 0.125 + 128);
}

Extended Advertising: BLE 5.0 Breaks the 31-Byte Barrier

BLE 5.0 introduced Extended Advertising, expanding the payload from 31 to 255 bytes. This is a game-changer for sensor beacons — you can now broadcast full telemetry, multi-sensor data, encrypted payloads, and even firmware update URLs in a single packet without GATT connections.

Feature Legacy (4.x) Extended (5.0+)
Max Payload 31 bytes 255 bytes
PHY Options 1M only 1M, 2M, Coded (125k/500k)
Advertising Sets 1 Up to 10 concurrent
TX Power Reporting Manual Auto in AUX_SYNC_IND
RSSI Stability ±4 dB typical ±2 dB with coded PHY

However, adoption is limited: only ~60% of smartphones in 2025 support BLE 5.0 extended advertising scanning. If your deployment targets consumer phones, you still need a legacy fallback. For infrastructure-grade deployments (dedicated gateways, RTLS anchors), extended advertising is the clear choice.

Advertising Interval: The Battery Life Equation

The advertising interval is the single most impactful parameter on battery life. Each transmitted packet costs energy; the interval determines how many packets per second:

// nRF52-series power consumption per advertising event
// (3 channels × 1M PHY, 0 dBm TX power)

const float TX_CURRENT_MA = 4.6;       // TX current at 0 dBm
const float TX_TIME_MS = 0.376;        // 376 µs per channel (preamble + header + 31B payload)
const float SLEEP_CURRENT_UA = 1.5;    // System OFF + RTC

// Energy per advertising event (3 channels)
float energy_per_event_uC = (TX_CURRENT_MA * 1000) * (TX_TIME_MS * 3 / 1000);
// = 4.6 * 1000 * 0.376 * 3 / 1000 = 5.19 µC per event

// Average current at 100 ms interval (10 events/sec)
float avg_current_100ms_uA = (energy_per_event_uC * 10) / 1.0 + SLEEP_CURRENT_UA;
// = 51.9 + 1.5 = 53.4 µA

// Average current at 1000 ms interval (1 event/sec)
float avg_current_1s_uA = (energy_per_event_uC * 1) / 1.0 + SLEEP_CURRENT_UA;
// = 5.19 + 1.5 = 6.69 µA

// CR2477 battery: 1000 mAh = 3600000 µA·s
// At 100 ms interval: 3600000 / 53.4 / 3600 / 24 = ~781 days
// At 1000 ms interval: 3600000 / 6.69 / 3600 / 24 = ~6235 days (theoretical)
Interval Avg Current CR2477 (1000 mAh) Life Detection Latency Use Case
100 ms 53.4 µA ~26 months ≤100 ms RTLS, real-time tracking
300 ms 18.8 µA ~71 months ≤300 ms Retail, interactive
1000 ms 6.7 µA ~6 years* ≤1 s Asset tracking, telemetry
5000 ms 2.5 µA ~16 years* ≤5 s Cold-chain, low-power

*Theoretical — self-discharge of CR batteries limits practical life to 5–7 years regardless of current draw.

The key insight: doubling the interval does not halve battery life. Sleep current (1.5 µA) is a fixed floor. At 100 ms, TX dominates (51.9 µA vs 1.5 µA sleep). At 5000 ms, sleep dominates (1.0 µA TX vs 1.5 µA sleep). The crossover point where TX current equals sleep current is around 3.5 seconds — beyond that, further interval extension yields diminishing returns.

Collision Probability: When Too Many Beacons Talk at Once

In a dense deployment (warehouse, hospital, conference hall), hundreds of beacons share the same three advertising channels (37, 38, 39). Each beacon transmits on all three channels simultaneously. When two transmissions overlap in time on the same channel, both packets are corrupted — a collision.

The collision probability follows a modified ALOHA model:

// Slotted ALOHA collision probability (approximation)
// N = number of beacons, T = packet air time, I = interval
//
// P_collision = 1 - exp(-2 * N * T / I)
//
// For 31-byte payload at 1M PHY: T = 376 µs per channel
// Beacons transmit on all 3 channels, so effective T = 376 µs
// (channels are separate frequencies, no inter-channel collision)

import math

def collision_prob(n_beacons, interval_ms, packet_time_us=376):
    # Convert to seconds
    T = packet_time_us / 1e6
    I = interval_ms / 1e3
    # Per-channel load (beacons distribute across 3 channels randomly)
    # Actually each beacon hits all 3 channels, so load per channel = N * T / I
    load = n_beacons * T / I
    p_collision = 1 - math.exp(-2 * load)
    return p_collision

# Examples:
# 50 beacons, 1000 ms interval:  P = 1 - exp(-2 * 50 * 0.000376 / 1.0) = 3.7%
# 200 beacons, 1000 ms interval: P = 1 - exp(-2 * 200 * 0.000376 / 1.0) = 14.0%
# 200 beacons, 300 ms interval:  P = 1 - exp(-2 * 200 * 0.000376 / 0.3) = 39.7%
# 500 beacons, 1000 ms interval: P = 1 - exp(-2 * 500 * 0.000376 / 1.0) = 31.3%
Beacons Interval Channel Load P(collision) P(receive on 1st scan)*
50 1000 ms 1.9% 3.7% 96.3%
200 1000 ms 7.5% 14.0% 86.0%
200 300 ms 25.1% 39.7% 60.3%
500 1000 ms 18.8% 31.3% 68.7%
500 300 ms 62.7% 71.5% 28.5%

*Probability of receiving at least one clean packet during a 1-second scan window, assuming 3 advertising channels and independent collision events.

Mitigation strategies for dense deployments:

  • Random jitter: Add ±20% random jitter to each beacon’s interval. This decorrelates transmission timing and breaks periodic collision patterns. Reduces P(collision) by 30–50% in practice.
  • Adaptive interval: Beacons detect congestion (by counting received packets from neighbors) and back off. Similar to CSMA/CA but for connectionless advertising.
  • Channel preference: Some implementations skip channel 37 (most congested due to Wi-Fi channel 1 overlap) and only advertise on 38+39. Reduces Wi-Fi interference at the cost of detection probability.
  • Extended advertising: BLE 5.0 secondary channels (used for AUX_SYNC_IND) operate on any of 37 data channels, spreading the load far more thinly than the 3 primary channels.

Multi-Frame Advertising: Rotating Payloads

When you need to broadcast more data than fits in a single 31-byte packet (legacy) or want to balance detection latency with telemetry freshness, use frame rotation. The beacon cycles through different frame types on successive advertising events:

Slot Frame Type Content Purpose
1 (every event) Identity frame Device ID + TX power + seq Fast detection, RSSI ranging
2 (every 3rd) Telemetry frame Battery + temp + humidity Environmental monitoring
3 (every 10th) Accelerometer frame XYZ + event counter + tamper Movement / shock detection
4 (every 30th) Info frame Firmware version + config hash OTA management, fleet audit

With a 1000 ms base interval, the identity frame transmits every second, telemetry every 3 seconds, accelerometer every 10 seconds, and info every 30 seconds. A scanner that listens for 3 seconds will capture both identity and telemetry; a 10-second scan captures everything except the info frame. This approach effectively multiplexes 4 logical beacons onto a single physical device with minimal battery overhead.

Conclusion

Advertising packet design is the highest-leverage engineering decision in a beacon deployment. A well-designed payload eliminates GATT connections, reduces scanner dwell time, and extends battery life by orders of magnitude. The principles are simple: pack identity and telemetry into the smallest possible footprint, use frame rotation for overflow, add jitter to survive dense deployments, and choose the interval that matches your latency budget — not faster.

For most asset-tracking and environmental monitoring deployments, the sweet spot is a custom 24-byte legacy frame at 1000 ms interval with ±20% jitter and 4-slot frame rotation. This configuration delivers 6-second detection latency (99th percentile), 5+ year battery life on a CR2477, and sub-5% collision rates up to 200 beacons per zone — numbers that no off-the-shelf iBeacon configuration can match.

BLE beacons are only as effective as the bytes they broadcast. Design the packet before designing the deployment.