10 Things Every Service Should Do Before Production
The incidents that hurt the most are almost never about a missing big feature. They are about a missing small one — a timeout never set, a retry without backoff, a health check that lied.
This is the checklist I run through before any service goes to production. For each item: what it is, and why it is needed.
1. Graceful Shutdown
On SIGTERM: stop accepting new requests, finish what is in flight, deregister from the load balancer, then exit.
Why it is needed: every deploy sends SIGTERM to a pod that is mid-request. Without a handler, those requests die as 502s — quietly, on every single deploy.
2. Health Checks (Liveness, Readiness, Startup)
Three probes, three jobs. Liveness: “are you alive” — fail and you get restarted. Readiness: “should I get traffic” — fail and traffic stops, but you keep running. Startup: “done booting” — until it passes, the other two wait.
Why it is needed: wiring a dependency check to liveness turns a database hiccup into a cascading restart of every pod. Readiness sheds traffic; liveness kills. Mixing them up is the most common Kubernetes misconfiguration there is.
3. Timeouts at Every Layer
A timeout on every network call, database query, and HTTP client — always lower than the timeout of whatever is calling you.
Why it is needed: the default in most libraries is “no timeout”. One downstream slows to 30s, your workers pile up waiting on it, and soon requests that have nothing to do with it are stuck too. Your service is down without anything having crashed. This is the single highest-leverage item on the list.
4. Retries With Backoff (And Limits)
Four properties: exponential backoff between attempts, jitter so clients do not retry in sync, a cap on total attempts (three is usually enough), and ideally a circuit breaker around it all.
Why it is needed: naïve retries make outages worse. A small hiccup turns into a sustained flood — every client retries, fails, retries again — and now the dependency cannot recover even after the original problem is gone.
5. Idempotency on Write Endpoints
An idempotency key: the caller sends a unique ID per logical action, the server stores it, and a duplicate ID gets the original result back instead of the work being done twice.
Why it is needed: the moment callers retry — and by the previous section, they will — a lost response means the same valid-looking request arrives twice. Without the key, that is a user charged twice.
6. Context Propagation
Generate a request ID at the edge, include it in every log line, pass it in headers to every downstream call.
Why it is needed: without it, a slow request is a mystery — “service A took 800ms” in one file, “service B took 600ms” in another, and no way to know they are the same request. One propagated ID makes the problem obvious.
7. Structured Logging
JSON or key-value logs with consistent field names across
services. One line of config in most logging libraries. No
DEBUG in production, no PII or secrets.
Why it is needed: it turns “grep for an hour across six services” into “filter by user_id in thirty seconds” — and converting later means redoing every grep, dashboard, and alert built on the unstructured logs.
The cost of doing it on day one is nothing. The cost of converting later is enormous, because every grep, every dashboard, every alert built on the unstructured logs has to be redone.
8. Rate Limiting and Backpressure
A per-client rate limit on every public endpoint, plus backpressure: notice when the queue grows faster than it drains, and shed work early instead of falling over.
Why it is needed: one client with a buggy retry loop can hit you ten thousand times a second and take the service down for everyone. And “what to do when overloaded” is much easier to design before you are overloaded.
9. Circuit Breakers on Dependencies
Count recent failures. Past a threshold, “open” the circuit and stop calling for a while. Then let one trial request through — success closes the circuit, failure keeps it open.
Why it is needed: failing fast is almost always better than timing out — your service stays responsive while a dependency is down, and the dependency gets room to recover instead of being hammered.
10. Safe Database Migrations
Additive first: add the column, then write to it, then read from it, then drop the old one. No long locks. Always a rollback plan. Every step safe with the previous version of the code still running.
Why it is needed: migrations are how most “the deploy broke prod” incidents actually happen — the code is fine, but the migration locked a table for two minutes or removed a column the old version still reads.
What NOT to Build Too Early
The other half of production-readiness is not over-engineering. Almost certainly not needed yet: dynamic config reload, unsampled tracing on every span, custom retry frameworks, feature flags on every line, multi-region active-active before your first incident.
Why it matters: each of these costs complexity today for a failure you cannot see yet. Build for the failures you can already see.
Closing
None of this is glamorous. But the gap between services that run quietly for years and services that page somebody every other week is almost entirely in this list.
Build for boring. Boring is what survives.