Back to blog
FILE 0xED·94% SPO2 IS NOT AN EMERGENCY

94% SpO2 is not an emergency

April 24, 2026 · aws, lambda, debugging, health

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:

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.