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:
- Parse the raw string into the declared type.
- Validate the result: clamp numeric ranges, reject
infandNaN, check enums. - Log any failure at
WARNINGwith the variable name, the raw value, and the default being used. - 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
| Situation | Do this |
|---|---|
| Value missing | Silent default — missing is normal, not an error |
Value malformed ("3O00") | Log WARNING with name + raw value, use default |
| Value out of range | Log and clamp to the nearest bound |
inf / NaN in a float knob | Reject as malformed — they pass float() but poison math |
| Secret / credential missing | Fail fast at startup — a default credential is worse than a crash |
| Feature flag unparseable | Treat 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
- Validate at startup, tolerate at read time. Run every reader once during boot so malformed values surface in the deploy logs immediately — then the runtime fallback is a second net, not the first.
- Use a schema library when knobs multiply. Pydantic's
BaseSettingsreads env vars, applies typed validation, validates even the defaults, and documents a clear source-priority order (init args → env vars → dotenv → secrets → defaults). One class becomes the single reader. - Make the default part of the contract. Google's SRE workbook pushes to "convert mandatory questions to optional ones" by providing defaults that "apply safely and effectively to most, if not all, users." Document the default next to the knob; an undocumented fallback is just a different surprise.
- Count the fallbacks. Emit a metric alongside the log line. One warning is a typo; a thousand per minute means your deploy tooling is shipping garbage.
Resources
- The Twelve-Factor App — Config
- Node.js docs —
process.envstring coercion - Pydantic Settings — typed settings from env vars, with validated defaults
- Google SRE Workbook — Configuration Design and Best Practices
- Python
logging—Logger.warning
Building production systems where a bad setting shouldn't become an incident? Yeda AI designs, audits, and ships them.