94% SpO2 is not an emergency
I got a high-severity SMS from my own safety app: "may be at risk. Factors: Low blood oxygen: 94%." 94% is normal. Consumer pulse oximeters have a ±2% accuracy floor. A 94% reading is indistinguishable from 96%. The server was waking up emergency contacts over noise.
What was happening
The server-side risk evaluator had two SpO2 thresholds:
const LOW_BLOOD_OXYGEN_PCT = 95; // HIGH severity → SMS
const CRITICAL_BLOOD_OXYGEN_PCT = 90; // CRITICAL severity
Anything below 95% fired a high-severity risk factor. The iOS
client was already using <90 to even surface it locally. The
server was the outlier — both more aggressive and acting on the
same data.
What I found
Clinical SpO2 reference ranges, as understood by a consumer device:
- ≥95% — normal
- 90–94% — mild hypoxemia, not alert-worthy on a consumer device
- <90% — true hypoxemia
- <85% — severe
A reading of 94% is at the noise floor of an Apple Watch or wrist pulse-ox. Treating that as a high-severity event means paging your contacts every time you sneeze near the sensor.
The fix
One file, two constants:
// risk_factors.php (was, now)
const LOW_BLOOD_OXYGEN_PCT = 90; // was 95
const CRITICAL_BLOOD_OXYGEN_PCT = 85; // was 90
Now the server matches the iOS client and the clinical reference ranges. Below 90% fires high; below 85% fires critical. Anything in the 90–94% range is no longer treated as a safety factor.
What I'd do differently
The original 95 came from "let's err on the side of caution." That's a fine instinct that produces terrible alert systems. Erring toward more alerts trains your user (and their contacts) to ignore the alerts entirely. The right side to err on for a safety system is the side of "every page that fires is actionable."
Also: client and server should derive the same constants from one source. The drift between iOS using 90 and the server using 95 had been live for a long time and nobody noticed because, until my pulse-ox dipped to 94% in the cron's evaluation window, the two never disagreed visibly.