14 — Operational resilience and external dependencies
Commandments III, V, VI at the runtime layer: prod lives in an unreliable world. The network drops, the provider blocks ports, the Scraping — Automatically gathering data from websites with a program instead of copying by hand. Powerful for acquiring data, but it needs manners: respect others’ rules (robots.txt), don’t overload the server. dies halfway, a paid API — An agreed way for two programs to talk to each other — one asks, the other answers in a set format. Through an API your app connects to outside services (payments, maps, AI). Treat an API key like a password. costs on every request. The doctrine in chapters 03–05 covers code and deployment; this chapter is about what happens after — when real users and independent systems hit a running service.
These lessons were paid for dearly: nearly every one is a prod incident, not theory. The common denominator — the worst bugs don’t shout, they quietly hang, log out, or drain the budget.
1. One bad request must not take down the process
An uncaught throw/rejection in an async handler can kill the entire server (Node/Express:
rejection → process exit → pm2 — A manager that keeps the (Node) app running all the time — restarts it after a crash. Without it, after a crash or server restart the app just sits idle. pm2 keeps it “alive”. restart loop). Build a net at the process level:
- Global catchers:
process.on('unhandledRejection')and'uncaughtException')— they log and shut down in a controlled way, leaving no zombie process. - An async-route wrapper that passes the error to
next(err)instead of losing it in an uncaught promise. - 500 middleware that doesn’t leak the stack trace to the user (→ 09).
- Defense on edge data: an OAuth account with no password, a
nullfield where the code assumes a string — these are real inputs once you let real users in (often surfaces after a database swap, → 05).
Rule: a throw in one request degrades that request, not the service. Test it for regressions (→ 03).
2. Long jobs: resumable and detached from the agent session
A “collect everything → save once” scraper/ETL loses 100% of its work on every crash. Write in batches:
- Checkpoint completed units (a file/table of URLs/IDs) — a restart resumes from where it left off, not from scratch.
- Save every N, not at the end — a crash costs the last batch, not the whole run.
- Run long jobs from a real terminal, not from a Claude session in the background. The agent’s background is a non-durable runner — tearing down the session host kills the process halfway (it’s not anti-bot, it’s a vanishing runner).
- End-to-end Idempotency — A property of an operation you can run many times with the same result — no duplication. Key for scripts and events: a re-run doesn’t break data. Like an “ON” switch. (→ 04): a resumed job doesn’t duplicate already-saved data.
3. Treat external sources as hostile
Other people’s APIs and pages drop connections, rate-limit (429), return 200 with an error page. Assume they’re unreliable:
- A timeout on every call — without it the socket hangs forever (a silent freeze, not an error).
- Retry with exponential backoff (e.g. 10/20/30 s), with an upper bound on attempts.
- Rotate session and User-Agent under heavy I/O — a fresh
Session(new TCP/cookies) per batch, a UA from a pool of real browsers (some hosts drop you after a few hundred requests from one session). - Validate the response, not the status — a “200 + error page” is a failure; check the shape of the data.
4. The provider’s infrastructure imposes limits — verify end-to-end
Hosting has its own network rules that break “working” code only in prod:
- Ports can be blocked. E.g. outbound SMTP 465 may be closed → use 587 + STARTTLS.
secure:trueon a blocked port hangs every send until timeout — set hard connection/greeting timeouts so the error is loud. - Mail deliverability isn’t “I sent it.” A verified sender domain (SPF/DKIM), a real test send, GDPR — The EU’s data-protection law — how you may collect, keep and delete users’ data. It concerns every app with people’s data. Better to write in consents and retention from the start than pay fines later.-compliant opt-in (→ 09). An email that “went out” but landed in spam/nowhere is a bug.
- Check this on the provider’s prod/staging, not locally — your local network doesn’t have these blocks (→ 03).
5. Every paid resource behind a hard quota
An Endpoint — A single “address” in an API you send a request to for a specific thing (e.g. the list of orders). Apps talk through endpoints — one endpoint = one function you expose. calling a paid API (LLM, geocoding, email) with no limit is an open tab and an abuse vector:
- A quota per user (monthly/daily window) with a clear message and a reset date up front.
- Differentiate per tier (free vs premium), enforce it server-side.
- Rate-limit + security headers (helmet/limiter) as a permanent part of the stack (→ 08).
- Tie it to the law and the terms of service: a limit and how you communicate it are also protection against abuse (→ 09).
6. Know prod is healthy — observability
The defenses above keep prod from dying; observability tells you it’s alive — after the deploy window closes, before a user emails you. “Verify, don’t declare” (→ 03) applied to a running system:
- Structured logs with levels.
error/warn/info(notprinteverywhere). Errors carry context (request id, user, what failed) — but never secrets or full PII. You grep logs at 2 a.m.; make them greppable. - A health endpoint.
/healthzreturning 200 + a cheap check (DB reachable, version) — for the load balancer and an uptime monitor. An external uptime ping tells you it’s down before the customer does. - Alert on what hurts, not on noise: spikes in 5xx, failed payments/emails, a job that didn’t run, the selector-drift / “200 + error page” case (→ § 3). One actionable alert beats a hundred dashboards.
- A few real metrics over vanity: error rate, p95 latency, queue depth, daily cost of paid APIs (→ § 5). Measure before you optimize (→ 13).
- Errors → a place you’ll see them (a log drain / error tracker), not just stdout that scrolls away.
Start tiny: logs with levels + a health check + one uptime ping covers most of the value. Grow only when a real incident shows the gap (capture the lesson in the runbook → 05).
Anti-patterns
- 🚫 No global error catcher — one bad request restarts the service for everyone.
- 🚫 A scraper with no checkpoints run from the session background — crash = run from scratch, vanishing runner = perpetual failure.
- 🚫 An external call with no timeout and backoff — a silent freeze or a ban after a string of 429s.
- 🚫 “I sent the email” ≠ I delivered it — no verification of port/domain/deliverability.
- 🚫 A paid API with no quota — a surprise bill and an open abuse vector.
- 🚫 Sessions in process memory — every deploy logs everyone out (→ 05).
- 🚫 No logs/alerts — you find out prod is down from the user, not the system; “it works” with no way to know.
- 🚫 Secrets/PII in logs — the log drain becomes the breach.
For new projects
Add to Day 0 (→ 07) before the first real user shows up: a global error handler + a 500 with no leak, timeouts and backoff on every external I/O, quotas on paid endpoints, a durable session store in a separate file. It’s cheaper now than as a 2 a.m. incident. Every prod incident → an entry in the runbook with a date and a numbered lesson (→ 05, 06).