You set your advertising interval to 100 ms. You flashed the device, put it on a power analyzer, and now you want to know what it costs you per hour.
So you do the obvious thing. Call it 10 advertising events per second, some amount of charge per event, multiply, done.
Except your device isn’t advertising every 100 ms, and it never was. On my bench, the same firmware configured for exactly 100.0 ms advertised at an average of 105.075 ms, and no two captures agreed. That’s not a bug in my setup, and it isn’t a quirk of the chip I happened to use. The Bluetooth specification requires it.
It doesn’t show up as an obvious failure. It shows up as a 5% error in a number that looks perfectly reasonable.
In this post, we’ll cover:
- What the specification actually mandates (and what it pointedly doesn’t)
- Why your measured period always runs long
- What that does to an average current figure, and the comparison where it does nothing at all
- How to read your real period off a capture
- Three things to do about it
What the specification says
The relevant text lives in the Core Specification, Volume 6, Part B, Section 4.4.2.2.1, and it defines the advertising event timing like this:
T_advEvent = advInterval + advDelayadvInterval is the value you configured.
advDelay is a pseudo-random value that the Link Layer
generates fresh for every advertising event, and the
specification bounds it to 0 to 10 ms.
A few things to note.
First, it isn’t optional and there’s no standard way to switch it off. There’s no bit in the specification that disables it, and the host API doesn’t expose a knob for it.
Some controllers do offer a vendor-specific escape hatch (Nordic’s
SoftDevice Controller has a Host Controller Interface (HCI) command,
hci_vs_sdc_set_adv_randomness(), whose rand_us
field is documented as “maximum random delay in microseconds, 0 to
disable randomness”). Using it puts you outside what the specification
requires, so treat it as a deliberate choice rather than a default.
Assume the delay is there unless someone removed it on purpose.
Second, it applies per event, not per advertising session. Your first event might carry 3 ms of delay, the next 9 ms, the next half a millisecond. There’s no pattern to lock onto.
Third, and this is the part that surprises people, it doesn’t apply everywhere. The specification scopes it to all undirected advertising events and to connectable directed advertising events used in a low duty cycle mode. High duty cycle connectable directed advertising is excluded, which makes sense given that mode exists precisely to reconnect fast.
What the specification carefully does not say
Two omissions matter here, and both of them get papered over in practice.
It never says the delay is uniformly distributed. It says pseudo-random, and it fixes the range. That’s all. So the moment you write “the expected value is 5 ms,” you’ve made an assumption the specification didn’t define. I’ll come back to this, because the assumption turns out to be a good one on the silicon I tested, and I did test it.
Section 4.4.2.2.1 states the requirement without giving a reason for it, which is why you’ll see the rationale asserted more confidently than it’s actually written. The specification does give one, though, a whole volume away: Volume 1, Part A, Section 3.3.2.2.2 says the advertising events “occur at regular intervals which are slightly modified with a random delay to aid in interference avoidance.” Cite that section if you need it, and keep the “aid in,” because the specification never claims the mechanism prevents collisions. It only says it helps.
What it does to the advertising period
Let’s put numbers on it. If we assume the delay is uniform across 0 to 10 ms, its expected value is 5 ms, a nominal 100 ms interval should produce an expected period of about 105 ms, and your advertising rate isn’t 10 events per second, it’s about 9.52.
On the bench, that looked like this. The firmware is a
non-connectable, non-scannable beacon (ADV_NONCONN_IND)
with the minimum and maximum advertising interval both set to 160 units,
which is exactly 100.0 ms. Setting minimum equal to maximum
matters: the controller is free to pick anywhere inside the range you
give it, so pinning the two together removes that freedom and leaves
advDelay as the only thing moving. The specification
recommends against equal values, so the controller can schedule around
its other radio activity, which makes this a deliberate measurement
choice rather than a default. A controller may schedule around the pin
anyway, and one that doesn’t support the value you pinned will reject
the command outright.
Across 10 captures of 30 advertising events each:
| Quantity | Value |
|---|---|
| Configured interval | 100.0 ms |
| Measured mean period, 10 captures | 105.075 ms |
| Range across those 10 captures | 104.25 to 106.42 ms |
| Excess over nominal | 5.075% |
That’s within a tenth of a millisecond of the 105 ms a uniform assumption predicts. And it isn’t a fluke of one session: a separate run two weeks earlier, on the same silicon, landed at 105.05 ms. I also checked whether those delays were spread evenly across the 0 to 10 ms window instead of bunching up at one end. Across 200 of them, they were.
So the uniformity assumption holds here. Note carefully what that is and isn’t: it’s a measured property of one controller, on one system on chip (SoC), at one SDK version, at one interval. It is not something the specification promises you, and a different vendor’s controller is entitled to distribute that delay however it likes inside the range.
Now let’s look at the range in that table. Those 10 captures are identical in every respect (same firmware image, same board, same supply, same session). The period still moved by more than 2 ms between them, because each capture drew its own fresh set of random delays.
The part that hits your power number
The charge your device spends on one advertising event is roughly fixed. It wakes, ramps the radio, transmits on three channels, and goes back to sleep. What varies is how often that happens.
Average current from advertising is approximately the charge per event divided by the period. So if you predict average current using the interval you configured while your device actually runs at 105.075 ms, you overestimate the advertising contribution by 5.075%. The general form is easy to carry around, so let’s write it down: the error is roughly 5 ms divided by your advertising interval. That 5 ms is the uniform assumption again, so the rows below inherit it, and on a controller you haven’t characterized they are an estimate rather than an exact figure.
| Advertising interval | Overestimate if you use the nominal |
|---|---|
| 20 ms (the specification’s floor) | ~25% |
| 50 ms | ~10% |
| 100 ms | ~5% |
| 500 ms | ~1% |
| 1 s | ~0.5% |
| 10 s | ~0.05% |
The top row is the one to remember, and it isn’t only arithmetic. In a separate session on the same part, a configured 20 ms advertised at 25.166 ms across 254 intervals, which is the quarter the table predicts. The mean delay there came out at 5.17 ms against 5.05 ms at 100 ms, so the delay does not scale with the interval, and that is exactly why its relative cost climbs as the interval falls. The specification fixes that range without fixing its average, so a controller whose delays cluster high could push a 20 ms interval to 50%. That is the ceiling the specification allows; a quarter is what this controller did. It falls away quickly above 20 ms, and somewhere around a second you can stop thinking about it.
Nordic teaches the same correction, if you want a second source before you trust it. In Designing Low-Power Bluetooth LE Products, Lesson 3’s advertising exercise extrapolates average current by dividing the event charge by 1005 ms for a 1000 ms interval, and by 105 ms for a 100 ms one, and it spells out that the delay lands on every interval regardless of length, so a configured 20 ms converges to 25 ms.
It also runs in the direction people find least intuitive. Your calculation says the device costs more than it does, so your battery life estimate comes out short, which is the conservative direction, so nobody investigates it. The error just sits there.
And there’s a second, sneakier version. If you capture a fixed window of time and divide by the number of events you think it contains, you’re making the same mistake with extra steps. For a free-running advertiser, a 20 s capture at a nominal 100 ms doesn’t hold 200 events; it holds about 190. (If your firmware sets a fixed event count, which some beacon samples do, then none of that arithmetic applies at all and the count is simply whatever you asked for.)
Where it does nothing, which is most of the time
Here is the part I got wrong in my first draft.
A systematic offset cancels out of a comparison. If you’re testing two firmware builds against each other, both at 100 ms, both carry the identical period factor, and it divides out of the ratio exactly. A 2% improvement measures as a 2% improvement whether you use 100 ms or 105.075 ms in the denominator. The offset does not eat your comparison, and it doesn’t eat half of it either.
It survives a little better when you compare two different intervals, but only a little. Going from 100 ms to 110 ms looks like a 9.09% saving on nominal figures and is really an 8.69% saving, so the delay accounts for 0.4 of a percentage point, or about 4.4% of the saving. It’s worth knowing, but it isn’t the headline.
What actually threatens a small comparison is the random half, not the systematic one. The run-to-run period spread I measured was 0.646%. Compare two captures and both of them carry it, so the difference between them carries more: 0.914%. Against the 2% improvement you were hoping to detect, that’s 46% of your signal, sitting there as noise, on a bench where nothing changed.
So the fix is to count more events, not to buy a better meter.
Why repeated measurements disagree
That scatter is predictable, and it is worth knowing where it comes from.
Every event draws its own delay, so no two captures ever see the same
set of them. Averaging across a capture pulls most of that out, but not
all of it, and what’s left is why 10 identical captures gave me 10
different periods. I measured that run-to-run spread at
0.646%, and advDelay accounts for most of
it.
Capture more events and the scatter shrinks: four times as many events, half the scatter. Going from 30 events to 120 does exactly that.
A better power analyzer does nothing for it, because the variation was never in your instrument.
The size of that scatter is also a diagnostic. From one event to the next the spacing moves by around 2.9 ms, and that figure doesn’t change when you change your interval, so the scatter you measure tells you what you’re actually looking at:
- Land near 2.9 ms and your interval is pinned and you’re seeing pure
advDelay. - Land far above that and something other than
advDelayis moving the advertising period. - Land near zero and the randomization isn’t being applied.
That last one has a deliberate cause (the vendor command above) and,
on Nordic’s SoftDevice Controller, an accidental one: known issue
DRGN-29306, where an advertising set that has previously run a high duty
cycle directed advertiser stops applying the random delay to its
undirected events, and stopping or restarting the set doesn’t bring it
back. Nordic’s documented way out is to call that same vendor command
with rand_us set to 10000, or to recreate the advertising
set.
How to actually read the advertising period
Let’s get concrete about where this number comes from, because “measure the advertising period” is easy to say and slightly less obvious to do.

On a power capture, the advertising events are the tall, narrow bursts and they’re unmistakable. What you want is the time from the start of one burst to the start of the next, averaged over as many as your capture holds. Don’t measure a single gap and call it the advertising period (a single gap contains a single random draw and tells you almost nothing). Take the first and last event, subtract, and divide by the number of intervals between them, which is one less than the number of events.
That last detail catches people. A 30 event capture has 29 gaps. Divide by 30 instead and the advertising period comes out about 3.3% low.
Now let’s notice which direction each error runs. The advertising delay pushes our measured period up by 5.1%. The off-by-one pushes it down by 3.3%. Make both mistakes at once and we get 101.6 ms, which is close enough to the 100 ms you configured that you’ll conclude the measurement confirms the datasheet, write it down, and move on.
Both errors are big enough to matter on their own, and together they cancel into an answer that looks fine.
If you’re working from a sniffer capture instead, group the packets into events before you do any of this. Nordic’s nRF Sniffer has a single radio, so it listens to one channel at a time. By default it waits on channel 37, and each time it catches a packet it hops to the next one, 38, then 39, then back to 37. A single advertising event transmits on all three in quick succession, so that hop pattern means one event can appear as up to three timestamped packets a millisecond or two apart, followed by the ~105 ms gap you actually care about. Divide by the packet count instead of the event count and you’ll get a period around a third of the real one, which is the same denominator mistake this post exists to prevent.
Either way, write the number down next to the measurement it belongs to. A period belongs to the capture it came from, not to your firmware, so reusing yesterday’s number is its own version of this mistake.
Three things to do about it
Measure the advertising period, don’t assume it. Every power capture that shows you advertising events also shows their spacing, and every sniffer capture timestamps them. The number takes about 10 s to obtain and it isn’t the number in your configuration file.
Normalize by the measured period when you compare. Every capture draws its own set of random delays, whether or not it shares a session with the one you’re comparing it against, so this applies as much to two builds captured 10 minutes apart as to two captured on different days. The systematic part already cancels when both sides sit at the same interval, so that isn’t what normalizing buys you. What it buys you is the period’s share of the run-to-run scatter. Whatever else your bench contributes is still there afterward, so treat this as removing one known term, not as making two captures interchangeable.
Average enough events that the delay stops mattering. If you want the period’s contribution below 0.2%, you need roughly 200 intervals, which at a measured 105 ms is about 21 s of capture. That is cheap.
One thing I don’t recommend: correcting for it analytically by adding 5 ms and moving on. The expected value is 5 ms, but any individual capture isn’t the expected value, and the variation is as much of a problem as the offset.
Where this doesn’t apply
A few boundaries, so you don’t over-apply this.
Periodic advertising is the way out, for the periodic
train. If you need a deterministic broadcast interval, this is
the mode that gives it to you: the periodic train carries no 0 to 10 ms
random delay, which is what lets a scanner wake in a narrow window
instead of listening wide. One thing to watch on a power capture,
though: the extended advertising set that announces the train is an
ordinary advertising set, and its primary channel events still carry
advDelay. You’ll see two rhythms, and only one of them is
jitter free.
Connections don’t work this way. Once a connection is established, connection events are anchored to the connection interval, and the timing behavior you care about is window widening driven by clock accuracy. The mechanism is different, and so is the math.
High duty cycle directed advertising is excluded, as above.
Extended advertising changes the packet structure and therefore the energy per event, though the interval mechanism still applies to the primary channel events. The period reasoning here holds, but every number about charge per event needs re-deriving for your configuration.
Your controller may be moving the interval too. I
removed that deliberately by pinning minimum and maximum to the same
value. If yours differ, the specification lets the controller choose a
value inside that range (the HCI parameters are documented as a range
precisely so it can pick one that fits its other scheduling), so your
baseline period may not be the minimum you asked for. Check your
configuration before you attribute everything you see to
advDelay.
Summary and closing
In this post, we covered why your advertising period is never the advertising interval you configured, and what that does to a power measurement.
We covered:
- The Core Specification mandates a pseudo-random
advDelayof 0 to 10 ms per advertising event, soT_advEvent = advInterval + advDelay - It applies to undirected advertising and to low duty cycle connectable directed advertising, and there’s no standard way to disable it (vendor-specific commands and periodic advertising are the exceptions)
- The specification fixes the range but never the distribution; assuming uniform predicts 105 ms at a 100 ms interval, and on the controller I measured that assumption held (105.075 ms, and 105.05 ms in a separate session)
- Predicting average current from the configured interval overestimates the advertising contribution by roughly 5 ms divided by your interval, about 25% at the 20 ms floor, 5% at 100 ms, and negligible by 1 s, where that 5 ms assumes a uniform draw of the kind I measured
- That offset cancels out of a same-interval comparison; what threatens a small comparison is the random part, which is 46% of a 2% signal
- Run-to-run variation shrinks as you capture more events, so more events fixes it and better instruments don’t
You should now be able to look at a power capture, tell how much of what you’re seeing is your device and how much is the protocol, and set up a comparison between two builds that isn’t quietly biased before you start.
That raises a further question: once you’ve removed the protocol’s contribution, how repeatable is what remains? That’s a measurement about your bench rather than about Bluetooth LE, and it’s worth its own post.