Where the two methods land
6 compared · 0 genuinely apart
Two estimates agreeing is worth more than either alone. Two disagreeing tells you which question to ask. Neither of them is ground truth — this app infers your threshold from the top of your hard running, Garmin infers it from the shape of your heart rate against your speed, and both are guesses about the same underlying thing.
Threshold heart rate
+4 bpm apart · agree
Both methods put your threshold within a few beats of the same place, which is about as much agreement as either can offer.
Threshold pace
-3.8% · near
Garmin's threshold pace detection moved from a chest-strap protocol to automatic detection from wrist heart rate in late 2024. Independent validation of the wrist-based version is thin, and one study measured it reading nearly 12% slow.
Race projections
two models, same runs
| Distance | This app | Garmin | Apart | |
|---|---|---|---|---|
| 5.0 km | 19:10 | 19:07 | +0.3% | agree |
| 10.0 km | 41:52 | 41:38 | +0.6% | agree |
| 21.1 km | 1:37:07 | 1:35:57 | +1.2% | agree |
| 42.2 km | 3:32:06 | 3:27:01 | +2.5% | near |
The two models agree at some distances and not others, which points at the shape of the curve rather than at its level -- the fitted fade exponent is where to look.
Garmin's own, with no equivalent here
No VO2max is computed here, and that is deliberate. A VO2max derived from heart rate and pace is a lossy compression of the heart-rate/pace relationship into one number using a population economy curve — it adds a familiar unit and a false precision, not information. Garmin's carries a mean error around 5–8% with a chest strap, and worse at both ends of the fitness range.
, so the check has to wait for the parsed document. addEventListener('DOMContentLoaded', function () { try { var dm = ['standalone', 'minimal-ui', 'fullscreen', 'window-controls-overlay']; if (!document.getElementById('frozen-note') && (navigator.standalone || dm.some(function (m) { return matchMedia('(display-mode: ' + m + ')').matches; }))) { var pk = 'portal-installed', reg = JSON.parse(localStorage.getItem(pk) || '{}'); reg.Run315 = Date.now(); localStorage.setItem(pk, JSON.stringify(reg)); } } catch (e) { /* private mode - ignore */ } }); // Installable, and readable on the train. See templates/sw.js. if ('serviceWorker' in navigator) { addEventListener('load', function () { navigator.serviceWorker.register('/Run315/sw.js'); }); }