What
An exact rrMs = 500 value appears ~20× more often than any neighbouring interval and is a
sentinel/fill, not a real beat — yet it is stored in rrInterval as a genuine interval with
tsSuspect NULL, on both the v18 historical and standard-profile transports.
Found while cross-checking R-R units on a WHOOP MG (fw 50.39.1.0), from a .noopbak backup
(314k labelled beats over 4 nights).
Evidence
Histogram around 500 ms (both WHOOP 5 channels, srcChannel 5 and 7):
495ms: 58 496ms: 45 497ms: 62 498ms: 51 499ms: 55
500ms: 1355 <-- ~20x its neighbours
501ms: 66 502ms: 74 503ms: 84 504ms: 78 505ms: 30
It cannot be a real interval — the strap's own HR contradicts it. 500 ms == 120 bpm, but at the
same-second hrSample the strap reports a resting rate every time:
mean strapHR at non-500 beats : 69.1 bpm
mean strapHR at 500 ms beats : 66.2 bpm (500 ms would require ~120 bpm)
top strapHR values at 500 ms beats: 60, 62, 61, 59, 64 bpm …
So the strap simultaneously asserts "≈66 bpm" and emits an interval meaning "120 bpm" for the same
instant. It skews slightly toward rest (66.2 vs 69.1), consistent with a fill emitted when the
sensor could not resolve a clean interval during quiet periods rather than a motion artifact.
Distribution: 1,355 beats across 1,027 distinct timestamps; on both transports (historical
878/165,565 = 0.53%, standard 477/148,566 = 0.32%); mostly isolated, with occasional short runs
(historical run-lengths {1:479, 2:78, 3:39, 4:15, 5:8, 6:2, 7:2}). tsSuspect is NULL on every
one of the 1,355.
Impact — scoped honestly
Shipped HRV and Charge are protected: both HRV paths run HRVAnalyzer.cleanRR
(range [300,2000] + Malik >20%-from-local-median), and a 500 ms beat among ~900 ms neighbours is
~44% off the local median, so it is rejected before RMSSD in the nightly (AnalyticsEngine) and
windowed (ResonanceEngine.scorePace) paths alike. This is not a "wrong HRV" report.
The concern is data hygiene:
- A known-bad fill is banked as a real interval with
tsSuspect unset — precisely the flag
meant to mark untrustworthy beats.
- It silently spends the ectopic-rejection budget (
HRVAnalyzer.maxRejectFraction). Tiny here
(0.3–0.5%), but it is noise eating headroom that a genuinely noisy night also needs.
- Any consumer that reads raw
rrInterval without running the full clean inherits a 44%-off
outlier.
- It pollutes the raw R-R corpus being banked for future R-R/HRV research, where a discrete spike
at one value is exactly the kind of artifact a later analysis would trip on.
Where it likely originates
The value is emitted at decode/ingest, since it carries a real ts and rides both transports. The
fix is one of: drop a beat that decodes to the fill, or set tsSuspect on it so every reader
inherits the doubt and the cleaners/telemetry can see it. Setting the flag is the smaller, more
conservative change and keeps the raw row for anyone studying the fill itself.
Deliberately not proposing the exact decode site without a second capture confirming 500 is the
literal fill on other firmwares — this is filed as a data observation with a proposed direction, not
a decoder patch.
Environment
WHOOP MG (DIS-attested), fw 50.39.1.0, iOS app 11.8.x build-from-source. Backup analysed locally;
raw values above are counts and interval lengths only — no health content. Reproducible from any
5/MG .noopbak with the query set on request.
Refs #1505, #1008.
What
An exact
rrMs = 500value appears ~20× more often than any neighbouring interval and is asentinel/fill, not a real beat — yet it is stored in
rrIntervalas a genuine interval withtsSuspectNULL, on both the v18 historical and standard-profile transports.Found while cross-checking R-R units on a WHOOP MG (fw 50.39.1.0), from a
.noopbakbackup(314k labelled beats over 4 nights).
Evidence
Histogram around 500 ms (both WHOOP 5 channels, srcChannel 5 and 7):
It cannot be a real interval — the strap's own HR contradicts it. 500 ms == 120 bpm, but at the
same-second
hrSamplethe strap reports a resting rate every time:So the strap simultaneously asserts "≈66 bpm" and emits an interval meaning "120 bpm" for the same
instant. It skews slightly toward rest (66.2 vs 69.1), consistent with a fill emitted when the
sensor could not resolve a clean interval during quiet periods rather than a motion artifact.
Distribution: 1,355 beats across 1,027 distinct timestamps; on both transports (historical
878/165,565 = 0.53%, standard 477/148,566 = 0.32%); mostly isolated, with occasional short runs
(historical run-lengths
{1:479, 2:78, 3:39, 4:15, 5:8, 6:2, 7:2}).tsSuspectis NULL on everyone of the 1,355.
Impact — scoped honestly
Shipped HRV and Charge are protected: both HRV paths run
HRVAnalyzer.cleanRR(range
[300,2000]+ Malik >20%-from-local-median), and a 500 ms beat among ~900 ms neighbours is~44% off the local median, so it is rejected before RMSSD in the nightly (
AnalyticsEngine) andwindowed (
ResonanceEngine.scorePace) paths alike. This is not a "wrong HRV" report.The concern is data hygiene:
tsSuspectunset — precisely the flagmeant to mark untrustworthy beats.
HRVAnalyzer.maxRejectFraction). Tiny here(0.3–0.5%), but it is noise eating headroom that a genuinely noisy night also needs.
rrIntervalwithout running the full clean inherits a 44%-offoutlier.
at one value is exactly the kind of artifact a later analysis would trip on.
Where it likely originates
The value is emitted at decode/ingest, since it carries a real
tsand rides both transports. Thefix is one of: drop a beat that decodes to the fill, or set
tsSuspecton it so every readerinherits the doubt and the cleaners/telemetry can see it. Setting the flag is the smaller, more
conservative change and keeps the raw row for anyone studying the fill itself.
Deliberately not proposing the exact decode site without a second capture confirming 500 is the
literal fill on other firmwares — this is filed as a data observation with a proposed direction, not
a decoder patch.
Environment
WHOOP MG (DIS-attested), fw 50.39.1.0, iOS app 11.8.x build-from-source. Backup analysed locally;
raw values above are counts and interval lengths only — no health content. Reproducible from any
5/MG
.noopbakwith the query set on request.Refs #1505, #1008.