Skip to content

#2358 replaced resting HR with mean sleeping HR on n=1 evidence: revert the definition, keep the nap fix #2522

Closed

Description

@DX23876

TL;DR: Of every resting-HR approach in use across the industry, #2358 picked the one that least isolates rest: an unweighted mean of the whole sleep session. The previous definition (lowest 5-min bin, which is Oura's definition) was one of the established ones. It may not be the best, since a smoothed or percentile nadir is more robust, but the current one is the worst of the lot. It raises every user's headline RHR by ~9–10 bpm, lowers Effort by ~10 %, moves Fitness Age by ~5 years, changes which nights Charge reads as recovered, shifts every other metric built on daily RHR, and fails this project's own validation rules by its own merge note. The nap fix in #2358 is genuine and should stay. The definition should go back to the lowest 5-minute bin, now taken from the primary session.

1. It no longer measures resting HR

Resting HR is HR in a steady state of minimal activity: a short, stable window at the quietest point. The pre-#2358 value was exactly that, the lowest 5-minute bin. The new value is the unweighted mean of every sample across the whole session: every sleep stage, every arousal, every wake minute inside the session boundaries. That is mean sleeping HR, a legitimate metric with its own name. Shipping it under the label "resting HR", and feeding it into recovery, is a category error, not a calibration choice.

2. No major wearable does this, and it ranks last among the approaches in use

Vendor Resting HR definition
Oura Lowest HR of the night. The API field is "Heart rate lowest (5-min window)", the exact definition #2358 removed. The night average is reported separately, not as RHR.
Garmin Lowest 30-min average in 24 h
WHOOP Dynamic average weighted towards the last slow-wave sleep
Polar Nightly Recharge: average of the first ~4 h of sleep. The official RHR is a manual supine morning measurement.
Apple Lowest HR during awake rest, sleep excluded

Every vendor either takes a nadir or restricts and weights the window towards the quietest state. None reports an unweighted whole-session mean as resting HR.

Ranked by how well they isolate the resting state: a smoothed or percentile nadir (Garmin; the 2025 methods study below) ≥ lowest 5-min bin (Oura; NOOP before #2358) > stage-weighted or window-restricted means (WHOOP, Polar) > unweighted whole-session mean (NOOP since #2358, used by no vendor). Apple measures awake rest, a different context, but it still targets the lowest resting value. #2358 moved NOOP from near the top of that list to the bottom.

3. No part of the night averages down to the resting floor, and #2284 would make it worse

Mean HR per stage of the primary session, relative to the same night's lowest 5-min bin (14 nights, one wearer):

Stage (as staged by NOOP) Mean HR Above the night's 5-min nadir
Deep 67.6 +11.3
Light 64.4 +8.1
REM 64.1 +7.9
Wake 72.6 +16.3

Every stage sits 8–16 bpm above the floor, so any average over sleep lands far above resting HR, by construction. Worse, segments staged as deep carry a higher mean HR than REM on 14 of 14 nights, which is physiologically backwards (staging error, or deep sleep clustering early before HR has fallen). A deep-weighted mean (#2284) would therefore push RHR further up. A nadir needs no staging and inherits none of its errors.

4. The evidence fails this project's own bar, by its own admission

  • One participant, five nights, rounded MAE.
  • The target was WHOOP's proprietary number. Agreeing with a black box shows the numbers look alike, not that the definition is valid.
  • The merge note itself calls the evidence "thinner than the change it carries".
  • AGENTS.md:

Prove the method tracks a varying input … Until it does, land it as instrumentation … or behind a default-off Experimental toggle — never make it the default or feed it a downstream gate (recovery, illness) on thin evidence.

#2358 did the opposite on every count: it made it the default, fed it into recovery, and shifted every existing user's headline RHR on their next score.

5. It changes which nights read as recovered

Charge scores RHR as a z-score against the personal baseline (W_RHR = 0.20), so the level shift largely cancels. The ranking of nights does not. Recomputed with RecoveryScorer over the last 14 nights (the recomputation reproduces the stored Charge exactly): with a strap-only baseline, individual nights move by up to 9 Charge points in either direction depending solely on the definition. Which mornings read as recovered is now decided by a definition that measures the wrong thing.

6. It also deflates Effort

Effort scores the day's HR as a share of heart-rate reserve, using that day's resting HR directly (restForStrain = restingHRDaily in AnalyticsEngine), with no baseline in between. Raising RHR by ~10 bpm shrinks the reserve, so the same activity counts as less intense. Example with HRmax 195: HR 120 is 46 % of reserve at RHR 56, but only 42 % at RHR 66. Low-intensity activity just above resting HR loses the most.

Recomputed with StrainScorer.strain (Edwards, the default) on identical day HR for 13 days: Effort is lower on every day under the #2358 definition, by 4.3 points on average (range 0.9 to 9.8) on the 0–100 scale. With typical daily Effort of 25–68 here, that is roughly 10 %, and over 20 % on single days. (This recomputation matches the stored Effort only within a few points, because the app assembles day HR slightly differently. Both variants run on the same input, so the difference itself is solid.)

7. Everything built on daily resting HR moves with it

Daily RHR is an input, not just a display value. Every consumer below reads the per-day value that #2358 redefined (checked on current main):

Consumer How it uses daily RHR Effect of the switch
Fitness Age / VO₂max estimate (IntelligenceEngine.fitnessAgeRows, HUNT model, Uth fallback) median RHR of the last 7 days +5.3 years from the RHR shift alone (same activity index, recomputed with FitnessAgeEngine). The stored value jumped +7.5 years in the week the change landed; the lower Effort also shrinks the activity index, so the two compound. The VO₂max estimate drops with it.
Effort (StrainScorer) heart-rate reserve see section 6
Charge (RecoveryScorer) z-score vs baseline see section 5
Day active-energy estimate (Calories.estimateDayCalories) Keytel fitness-adjusted, VO₂max from Uth (15.3 · HRmax / RHR) the Uth VO₂max falls by ~15 % for this shift, so energy estimates move with it
Workout detection (WorkoutDetector.detect(restingHR:)) thresholds relative to RHR a higher RHR raises the bar for what counts as a workout
Live sessions (repo.today?.restingHr) live zones and live Effort zones shift up
Vitality median RHR shifts
Apple Health write-back (HealthKitBridge) writes the day's RHR to Health ~9–10 bpm higher values exported to Health and visible to every other app
Coach context (AICoach) daily RHR and 30-day average in the prompt the coach reasons about an inflated RHR
Illness signal, Readiness, cycle phase z-scores vs personal baselines level largely cancels once baselines catch up; individual days shift

One caveat, to be precise: the HUNT model behind Fitness Age was built on an awake seated resting HR (NTNU's calculator: sit quietly for ten minutes, take the lowest pulse), which neither nocturnal estimate is exactly. That is a reason to choose that input deliberately, not to let a redefinition move Fitness Age by five years as a side effect.

Data (one wearer, WHOOP 5.0, last 14 nights; an illustration, not validation)

  • Paired, night by night: the new value is higher on every night, by +9.4 bpm on average (median +9.5, range +6 to +12).
Estimator (primary session) Mean Night-to-night SD
Whole-session mean (#2358) 65.7 3.3
Lowest 5-min bin (pre-#2358) 56.0 2.9
Lowest 30-min average (Garmin-style) 58.8 2.9
1st percentile of 30-s means 54.8 2.7
5th percentile of 30-s means 57.4 2.9

Every nadir-type estimator lands within ~4 bpm of the others. The mean sits alone, ~9–10 bpm above all of them.

"But the minimum is artifact-prone"

That objection applies to a raw minimum. A 2025 methods study (Digital Health, 10 participants, chest ECG) found that minima of 15–60 s windows produced non-physiological values (≤ 30 bpm) for 40 % of participants, and it recommends the 1st percentile of 30-s means. A 5-minute bin already averages ~300 samples. The answer to a noisy nadir is a robust nadir, not abandoning the nadir for the mean.

Recommendation

  1. Keep the primary-session selection from fix(analytics): wire primary-session resting HR to fix nap floor dist… #2358 (the nap fix).
  2. Keep the lowest 5-min bin as the resting-HR definition, now taken from the primary session, i.e. revert the definition part of fix(analytics): wire primary-session resting HR to fix nap floor dist… #2358. A smoothed or percentile nadir is a reasonable later refinement; the mean is not.
  3. Keep the session mean as a separately labelled "average sleeping HR" (instrumentation or display), never as RHR.
  4. Any future redefinition, fix(rhr): report resting HR as the deep-sleep mean, not the night's lowest 5-min bin #2284 included, ships behind a default-off toggle until it is shown to track a varying input across several people, as AGENTS.md requires.

Credit to @moyasserr for finding the nap bug. That part is a real fix.

Refs #1169, #2358, #2284.

Sources: Oura API metrics · Garmin RHR · WHOOP RHR · Polar Nightly Recharge · Polar manual RHR · Apple Watch HR · RHR methods study, Digital Health 2025 · Wearable nocturnal RHR validation · NTNU fitness calculator (HUNT)

Edited: rewritten; data restricted to the last 14 nights (the wearer's fitness changed, so earlier nights aren't comparable), device corrected to WHOOP 5.0, per-stage, Charge and Effort analysis and the downstream-metrics table added, and an earlier claim about a mixed baseline withdrawn after checking how the baseline is built.

Activity

  1. changed the title [-]#2358: keep the primary-session fix, but restore a nadir definition of resting HR[/-] [+]#2358 replaced resting HR with mean sleeping HR on n=1 evidence: revert the definition, keep the nap fix[/+] on Sep 27, 2026
  2. DX23876 commented on Sep 30, 2026

    @DX23876
    Author

    Fixed by #2557 (thanks @kavemang): the daily resting HR is the primary night's five-minute floor again, as this issue proposed. Closing as completed.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions