Yeda AI Tips · #106

Español

A Malformed Config Value Should Log and Fall Back

A typo in an env var just took down production. Not a bad deploy, not a bug in business logic — a human typed TIMEOUT_MS=3O00 (that's a letter O) and the next int() call raised an exception in the middle of a request. A config layer whose whole point is safety must never become the outage. The fix is one rule: a malformed config value is logged and degraded to a documented default — it never crashes a request.

Why scattered reads are a time bomb

Environment variables are the standard place for deploy-time config — the Twelve-Factor App methodology recommends them precisely because they change between deploys without code changes. But they come with a catch: they are always strings. Node's process.env even coerces assigned values to strings (env.test = null reads back as 'null'), and every language makes you parse "3000", "true", or "0.25" yourself.

When those parses are scattered — an int(os.environ["TIMEOUT_MS"]) here, a parseFloat there — every call site is a separate crash point, and each one fails at the worst possible moment: at request time, in production, when the value is finally read. One bad value can take down a worker pool that was otherwise healthy.

The mechanism: one typed reader

Route every knob through a single reader that owns four jobs:

  1. Parse the raw string into the declared type.
  2. Validate the result: clamp numeric ranges, reject inf and NaN, check enums.
  3. Log any failure at WARNING with the variable name, the raw value, and the default being used.
  4. Fall back to a documented default and keep serving.
import logging, math, os

log = logging.getLogger("config")

def read_float(name: str, default: float, lo: float, hi: float) -> float:
    raw = os.environ.get(name)
    if raw is None:
        return default
    try:
        val = float(raw)
        if not math.isfinite(val):
            raise ValueError("non-finite")
    except ValueError:
        log.warning("config %s=%r malformed; using default %s", name, raw, default)
        return default
    if not (lo <= val <= hi):
        log.warning("config %s=%s out of range [%s, %s]; clamping", name, val, lo, hi)
        return min(max(val, lo), hi)
    return val

RETRY_BACKOFF = read_float("RETRY_BACKOFF", default=0.5, lo=0.05, hi=30.0)

Six lines of policy, applied everywhere: parse, finite-check, log, default, clamp, return. The WARNING line is the whole observability story — a bad setting becomes something you grep for in the morning, not a page at 3 a.m.

Rules of thumb

SituationDo this
Value missingSilent default — missing is normal, not an error
Value malformed ("3O00")Log WARNING with name + raw value, use default
Value out of rangeLog and clamp to the nearest bound
inf / NaN in a float knobReject as malformed — they pass float() but poison math
Secret / credential missingFail fast at startup — a default credential is worse than a crash
Feature flag unparseableTreat as off — the safe direction, not the exciting one

The last two rows matter: fail-safe is not fail-open. Values where a wrong guess is dangerous (credentials, security toggles) should fail loudly at boot, where the blast radius is a failed deploy — never mid-request.

Power-user moves

Resources

Building production systems where a bad setting shouldn't become an incident? Yeda AI designs, audits, and ships them.

Talk to us · Read the blog