Release It! Design and Deploy Production-Ready Software
Contents
- Overview
- Key Claims
- Chapter Notes
- Chapter 17 — Chaos Engineering
- Chapter 16 — Adaptation
- Chapter 15 — Case Study: Trampled by Your Own Customers
- Chapter 14 — Handling Versions
- Chapter 13 — Design for Deployment
- Chapter 12 — Case Study: Waiting for Godot
- Chapter 11 — Security
- Chapter 10 — Control Plane
- Chapter 9 — Interconnect
- Chapter 8 — Processes on Machines
- Chapter 7 — Foundations
- Chapter 6 — Case Study: Phenomenal Cosmic Powers, Itty-Bitty Living Space
- Chapter 5 — Stability Patterns
- Chapter 4 — Stability Antipatterns
- Chapter 3 — Stabilize Your System
- Chapter 2 — Case Study: The Exception That Grounded an Airline
- Chapter 1 — Living in Production
- Related Pages
Authors: Michael Nygard
Published: 2018 (2nd edition; 1st edition 2007)
Slug: release-it
Overview
Michael Nygard's Release It! is the practitioner's guide to building software that survives production. Where most architecture books describe what systems should do, this book addresses what they should not do: crash, hang, lose data, cascade failures, or require human intervention at 3 a.m.
The book's central argument is that "design for production" must be a first-class design activity alongside feature development. Passing QA tells you almost nothing about a system's long-term viability; the real test is years of production operation under hostile conditions. Architectural decisions are simultaneously financial decisions — the operational cost of poor choices accumulates over the entire system lifespan.
The book is structured around three broad themes: (1) Stability — preventing small failures from cascading; (2) Capacity — handling load, connections, and resources gracefully; (3) Production readiness — deployment, versioning, adaptation, chaos engineering. Nygard draws heavily on real production incidents — all anonymised but concrete — to motivate each pattern.
The audience is experienced developers and architects building systems that must run unattended in production, often at scale, often with interconnected dependencies. The book predates the current microservices era but anticipated nearly all of its operational concerns.
Key Claims
- "Feature complete" is not "production ready" — QA tests what software should do; production tests what it should not do (ch. 1)
- Bugs cannot be eliminated; they must be survived — design for resilience, not prevention (ch. 2)
- Design and architecture decisions are financial decisions — operational cost accumulates over years; avoiding a one-time $50k investment to save deployment downtime often costs $1M+ (ch. 1)
- Small failures in interconnected systems propagate and amplify — the goal is to prevent failures in one system from cascading to all others (ch. 2)
- The pragmatic architect thinks continuously about deployment, metrics, and change dynamics — not end-state perfection (ch. 1)
Chapter Notes
Chapter 17 — Chaos Engineering
Chaos engineering is "the discipline of experimenting on a distributed system in order to build confidence in the system's capability to withstand turbulent conditions in production." Empirical, not formal: run experiments to learn what the system does, not models of what it should do.
Why production is necessary: staging environments cannot replicate production at scale; certain failure modes only emerge in the full system (cascading failures, dogpile effects, retry storms). More importantly, safety is not a composable property — two services that are individually safe may not be safe in composition. A sequential call to two services, each with P99.9 latency of 30 ms under a 50 ms combined timeout, will breach the budget for a meaningful fraction of requests. This is invisible from component testing.
Theoretical roots:
- Drift into failure (Dekker): economic pressure pushes systems toward their safety boundary over time; highly efficient systems break catastrophically when disrupted. Chaos is the countervailing force.
- Fundamental regulator paradox (Weinberg): the better a regulator suppresses variation, the less feedback it receives about its own quality. If stability mechanisms are never exercised, expertise in handling failures atrophies.
- Volkswagen microbus paradox: you learn to fix what breaks often; rare failures remain mysterious and are worse when they eventually occur.
- Antifragile (Taleb): use controlled stressors to build strength, like a weightlifter.
Netflix Simian Army: Chaos Monkey (random instance termination), Latency Monkey (latency injection), Janitor Monkey (resource cleanup), Conformity Monkey (standards validation), Chaos Kong (full region termination). Opt-out model: every service is subject by default; exemptions require sign-off and carry review stigma.
Prerequisites: (1) no irreplaceable requests; (2) blast radius control; (3) distributed tracing to distinguish fault-tolerant success from failure; (4) monitoring that can detect small failure-rate changes ("a wall full of green dashboards means monitoring isn't good enough" — Charity Majors); (5) recovery plan for when chaos stops.
Experiment design: hypothesis → steady state definition → rejection criteria → inject → observe vs control group. Account for baseline noise before concluding a failure was caused by the injection.
Injection types: instance termination (basic — finds autoscaling gaps), latency injection (finds timeout omissions and race conditions from out-of-order responses), service-call failure via FIT — Failure Injection Testing (Netflix: tag request at API gateway with "fail when G calls H"; at the call site the framework reports failure without making the network call; requires a common outbound calling framework).
Targeting progression: start with random injection (densely populated bug space); as easy bugs are fixed, switch to targeted injection based on call-tree knowledge. Learn from cases where faults don't cause failures — they reveal redundancy paths worth documenting.
Cunning malevolent intelligence (Peter Alvaro, UCSC): collect traces → build dependency graph → use graph algorithms to find crucial links → learn from redundancy discovered. Narrows the search space as the system matures.
Automation and moderation: automate once a vulnerability class is understood; apply constraints (never kill the last instance; don't fail all fallbacks simultaneously). A chaos automation platform (ChAP at Netflix) manages injection scheduling, victim selection, safety limits, and reports test events to monitoring for correlation.
Disaster simulations for humans: randomly designate 20–50% of staff as "zombies" (unavailable). Observe what operations cannot proceed. Review like a postmortem; fix through documentation, role distribution, automation. Escalate by combining with system fault injection once the organisation can handle normal zombie rates.
Chapter 16 — Adaptation
The broadest chapter in the book, covering the organizational, architectural, and information-level changes needed to make systems adaptable over time.
Decision loop speed as competitive advantage: every organisation runs a sense → decide → act → observe cycle. Getting around the cycle faster than competitors means forcing them to react to you ("inside their decision loop"). Agile removed delay from the "act" phase; DevOps removes more and adds "observe" tooling. The next frontier is the "decide" phase.
Thrashing: when your action rate exceeds the feedback rate from the environment, organisations oscillate — constantly shifting direction before the previous change has been evaluated. Aviation calls this "pilot-induced oscillation" (porpoising). Fix: don't slow down delivery; speed up feedback collection instead.
Platform team vs "DevOps team" antipattern:
- Platform team: infrastructure-as-a-product for app developers (compute, storage, logging, metrics, messaging, DNS, access control); provides APIs/CLI — not manual provisioning; measured on platform availability, not application availability. Rule: if developers only use the platform because it's mandatory, the platform isn't good enough.
- "DevOps team" antipattern: putting an intermediary between dev and ops creates two interfaces where one existed; DevOps is a cultural transformation, not an organizational unit.
Painless releases and service extinction: frequent releases force you to get good at releases (closed feedback loop). The most important part of evolution is extinction — shut down the underperforming service, delete the code, reassign the team. Preserves capacity and reduces dependencies.
Team-scale autonomy: a "two-pizza team" is not just about team size — it's about reducing external dependencies so a small cross-functional team can push code to production without coordinating with others. Coordinated deployments are a coupling smell: if provider and consumer must be updated simultaneously, rework the interface for backward compatibility.
Beware efficiency: high utilization (everyone 100% busy) is not the same as efficient throughput — keeping all workers fully loaded creates queueing delays that slow the overall system. Efficiency also specialises the process to today's tasks, reducing flexibility for tomorrow (the car manufacturer rig example: faster production of current models, impossible to switch to vans).
Form follows failure (Henry Petroski): design evolution is motivated more by what the prior version does poorly than by what it does well. The same applies to software architecture — systems evolve through failures, not through the initial design.
Bad Layering (horizontal coupling): standard layered architectures enforce vertical isolation (UI / domain / persistence) but encourage horizontal coupling — large "god" domain classes that everything touches. A better decomposition rotates 90 degrees: component-based or "self-contained systems" where each component owns its full stack (UI through storage) and communicates only through narrow formal interfaces.
Architecture styles for adaptability: microservices (small disposable units; team-scale autonomy; vulnerable to platform coupling for monitoring/tracing), microkernel+plugins (in-process message passing; good for incremental requirements change), event-based (temporal decoupling; allows new subscribers without publisher change; vulnerable to semantic change in message formats over time).
Modular operators (Baldwin & Clark, Design Rules): six operators that create options at module boundaries: Splitting (break module into submodules; interface unchanged), Substituting (replace with another module sharing the interface), Augmenting (add a module), Excluding (remove a module), Inversion (extract distributed functionality to a first-class concern — e.g., each service doing its own A/B tests → extract to a centralised experimentation service), Porting (reuse a module from a different system). Economic value of the system increases with the number of option boundaries.
Information architecture for adaptability:
- Messages, Events, Commands (Martin Fowler taxonomy, cited by Nygard): Event notification (fire-and-forget), Event-carried state transfer (entity replication), Event sourcing (change journal), CQRS (read/write separation). Use open formats (JSON) for events; avoid serialised objects, code-generation frameworks, or annotation-based mappers — treat messages as data, not objects.
- Services control their identifiers: don't couple to an external authority's ownership model; issue your own IDs; use a policy proxy to map caller identity to resource identity. This makes the service usable in more contexts without prior knowledge of the caller's auth model.
- URL dualism: a URL serves as both an opaque token (pass-through) and a dereferenceable address. Using full URLs as identifiers allows the front end to work with services that didn't exist when it was written — including static files or partner APIs — without hardcoding which service to call.
- Embrace plurality / federated zones of authority: every important noun will have multiple definitions across the organisation (a "customer" means different things to sales, support, and accounting). Don't create an enterprise-wide single system of record — it will always miss some facet. Allow federated data ownership with common interchange formats.
- Avoid concept leakage: don't expose internal data-model concepts to downstream systems. The "price point" example — an internal leverage concept for bulk repricing — leaked into every downstream system's data feed, causing years of coupling. Flatten when publishing; downstream consumers should receive resolved values, not the upstream's internal model.
Chapter 15 — Case Study: Trampled by Your Own Customers
A big-bang launch case study motivating noise testing, production-oriented configuration, and safety devices. A 300-person retail e-commerce relaunch: three months of load testing, target of 25,000 concurrent sessions (revised down to 12,000 during testing). At 9:05 AM launch: 10,000 sessions. By 9:10 AM: 50,000. By 9:30 AM: 250,000. Site crashed.
Building to pass tests, not to run in production: config files were written for the QA topology; hostnames, ports, DB passwords scattered across thousands of files; some components assumed QA-specific topology that wouldn't exist in production (no firewalls; different clustering). "The development teams were building everything to pass testing, not to run in production." Fix: configuration override structure separate from the codebase — each environment-varying property exists in exactly one place; production credentials kept out of QA and source control.
Sessions ≠ users: sessions always outlive users (sessions are kept alive by a timeout after the last click). Counting active sessions overestimates actual concurrent users. Session count is the most important metric for app server health but must not be confused with user count.
Why the site crashed — four sources of noise:
- Old URL redirects: CDN switch drove ~40% of traffic from search engine caches using old-style URLs → each request created a session just to serve a 404.
- Session IDs in URL query parameters (not cookies): a security team mandate — but search engine spiders, which don't handle cookies, were then creating new sessions per request (up to 10 sessions/second from one spider alone).
- Shopbots and scrapers: didn't handle cookies → sessions per request; used cloaking (multi-subnet, rotating user agents) to evade detection.
- Random weird traffic: unexplainable request patterns that still created sessions.
The testing gap: load test scripts were "polite" — they used cookies, followed linked URLs in order, obeyed the implicit rules. No noise (bots, cookieless requests, 404 floods, repeat-URL hammering). "Add noise, create chaos." Also: no safety devices in the application — when requests failed, threads piled up rather than being cut off; the team saw this pattern from day one of load testing but dismissed it as a test-methodology artifact.
Rapid aftermath mitigations (CDN gateway page): (1) cookie detection — redirect cookieless clients to an explanation page; (2) session throttle — configurable percentage of new sessions allowed through, rest get a "come back later" page; (3) IP block list for identified scrapers. Plus: static home page for unidentified users (the fully dynamic home page required 1,000+ DB transactions per render; personalization was never used for anonymous visitors); turned off session failover after discovering sessions held serialised full shopping carts and 500-item search results.
"Nothing is as permanent as a temporary fix": rolling restarts and the static home page persisted for years; the longest lasted a decade through 100% team turnover. Every mitigation had a direct cost: throttling lost revenue; no session failover meant abandoned carts at checkout; static home prevented personalization.
The headline finding: two years after launch, the site handled four times the original load on fewer servers of the same model. Nygard's point: if the application had been built production-ready from the start, the engineers could have joined marketing's launch party.
Chapter 14 — Handling Versions
A good citizen API does extra work on the provider side to avoid pushing migration costs onto consumers. The chapter's framework: communication is a layered stack of agreements (connection handshaking, request framing, content encoding, message syntax, message semantics, auth); a breaking change is any unilateral break from a prior agreement.
Postel's Robustness Principle applied to APIs: "Be conservative in what you do, be liberal in what you accept from others." This creates an asymmetry between inbound and outbound: you can always accept more, but never less; you can always return more, but never less.
Non-breaking changes (always safe):
- Require a subset of the previously required parameters
- Accept a superset of the previously accepted parameters
- Return a superset of the previously returned values
- Enforce a subset of the previously required constraints
Breaking changes (must trigger a major version):
- Rejecting a protocol, encoding, or request syntax that previously worked
- Adding required fields to the request
- Forbidding optional input that was previously allowed
- Removing previously guaranteed response fields
- Requiring an increased level of authorization
Implementation is the de facto spec: once live, the service's actual behaviour — including bugs and undocumented edge cases — is the real contract. Adding validation that rejects previously-accepted (if malformed) input is a breaking change, regardless of what the documentation said. Postel's Principle leaves no choice: keep accepting it.
Inbound + outbound contract testing: inbound tests exercise your own API against your understanding of the spec; outbound tests exercise your dependencies using their spec but your interpretation. Both gaps can be large even with a written spec. Contract tests owned by the consuming team (not the implementing team) are more effective — the consumer will discover misunderstood edge cases the implementing team would miss.
Breaking changes — versioning approaches (Nygard's preference: URL versioning):
- URL prefix (
/v2/...) — visible, loggable, routable by any intermediary; "not RESTful" but practical. - Accept/Content-Type header — RESTful but requires consumers to know special media types; hard to route at the load balancer.
- Custom version header — flexible but more secret knowledge for consumers.
- Field in body — easy for POST/PUT but doesn't cover all cases.
URL versioning wins because: the URL alone is sufficient (no secret knowledge); intermediaries (caches, load balancers) route on URL patterns without special configuration; useful for log analysis by version.
Rules when bumping versions: bump all routes at the same time, even if only one changed — don't force consumers to track which version number applies to which route. Run tests mixing calls to old and new API versions on the same entities (entities created with the new version often cause errors when accessed via the old API).
Version translation in controllers: methods for the old API convert old objects to the current domain model on inbound requests and convert current objects back to old on responses. Keeps business logic deduplicated; only the controller layer does translation.
Chapter 13 — Design for Deployment
"We don't just write for the end state and leave it up to operations to figure out how to get the stuff running in production. We treat deployment as a feature." Deployment must be a first-class engineering concern, not an afterthought that operations handles.
Continuous deployment virtuous cycle: large, infrequent deployments → high risk per deployment → review gates, freeze windows, ceremonies → deployments get larger and riskier. Break the cycle by deploying more often. "If it hurts, do it more often." Smaller deployments mean smaller blast radius, faster rollback, lower cognitive overhead, and — crucially — an immediate financial benefit: eliminating the "deployment army" described in ch. 12.
Four microscopic deployment phases (per instance):
- Prepare — copy new files or deploy new container image to the instance without disrupting the currently running version; no traffic changes yet.
- Drain — stop the instance accepting new requests; wait for all in-flight requests to complete (configurable grace period; hard cutoff if requests exceed it).
- Apply — perform the actual cutover (symlink swap, container restart, DB schema step); should be near-instantaneous.
- Start — boot new version with "accepting work" flag false; wait for health check to go green; only then notify load balancer to resume traffic.
Starting with the accepting-work flag false prevents a race where health checks pass before the application is truly ready.
Relational database — expand/contract pattern:
- Expansion phase (safe to run before any code deployment): add new tables, views, nullable columns, column aliases, stored procedures, and triggers; copy existing data to new columns.
- Database shims: triggers that keep old and new tables in sync during the mixed-version window when old and new code are both running.
- Contraction/cleanup phase (only after full rollout and confidence in stability): drop old tables, views, and columns; apply NOT NULL and FK constraints that couldn't be added during expansion.
Schemaless database migration — three approaches:
- Version pipeline: read documents at any historical version, translate up through a chain. Theoretically complete but expensive to maintain as versions accumulate.
- Full batch migration: migrate all documents before deployment. Works for small datasets; takes too long later.
- Trickle, then batch (preferred): migrate documents on read during normal operation; run a batch sweep on documents not touched by normal traffic after the deployment settles; remove migration code in the next deployment.
Web assets: cache busting is essential — embed a commit SHA or content hash in the URL path (not query string), since some CDNs and proxies strip query strings when caching. Ensure assets are deployed to all hosts before code changes reference them, or use session affinity so a user is not served mismatched asset and HTML versions.
Canary group: the first batch of instances in a rolling deployment is the canary group. Pause after the canary group to evaluate SLIs (error rates, latency, memory usage) before proceeding with the wider rollout. Health-check-based graceful drain (phase 2 above) allows the pool to be removed cleanly rather than abruptly killing in-flight requests.
Chapter 12 — Case Study: Waiting for Godot
A case study that motivates automated deployment pipelines through contrast.
A scheduled maintenance deployment for a retail e-commerce site: 40+ people on a conference bridge, 24 hours of effort, a 126-row playbook reviewed in two-hour meetings, business stakeholders woken at 1 a.m. to do UAT. Cost: ~$100,000 per deployment event (40 people × 24 hours × ~$100/hour) done 4–6 times per year ($400–600K/year). The deployment failed UAT because data in the QA environment didn't match production — JavaScript from a third-party widget rewrote page content that conflicted with the new structure, a problem invisible in QA. Rollback at 5 a.m.; second attempt planned two days later.
Nygard contrasts this with witnessing Etsy's "deployinator" — a one-button deployment, demonstrated live to an investor as a routine event. The contrast reveals the true cost of the "deployment army" antipattern: not just money, but wasted human potential, disrupted lives, and the opportunity cost of treating humans as bots.
Motivates: Chapter 13 (Design for Deployment), Chapter 14 (Handling Versions) — making deployments faster and more routine creates a virtuous cycle and an immediate financial benefit.
Connection: reinforces the ROI argument from ch. 1 — the $100K per deployment army × 5/year = $500K/year is the "invisible cost" Nygard argues architects must account for when designing for production.
Chapter 11 — Security
Coverage of the OWASP Top 10 (2017) and production security principles.
OWASP Top 10 (2017):
- Injection — SQL, XXE, Eval, XPATH; use parameterized queries and prepared statements, never string concatenation; disable XXE in XML parsers explicitly.
- Broken Authentication and Session Management — session IDs in cookies (not URLs), fresh session ID on authentication, PRNG session IDs with high entropy, hash passwords with salt, limit authentication attempts without over-aggressively locking accounts.
- XSS — escape output, never render user input directly into HTML; same injection-of-structured-data principle as SQL injection; automated scanners find XSS in milliseconds.
- Broken Access Control — don't use sequential DB IDs in URLs; authorize on every request, not just by URL possession; return the same 404 whether a resource doesn't exist or the caller isn't authorized (don't leak existence information).
- Security Misconfiguration — default credentials (20,000 MongoDB instances taken hostage in 2017); servers listening too broadly (bind to specific NICs); sample applications in production; ensure every administrator uses a personal account.
- Sensitive Data Exposure — don't store what you don't need; TLS everywhere including internally ("pie crust" defence fails — attackers behind the perimeter can sniff internal traffic); HSTS; encrypt at rest; decrypt based on user authorization, not server privilege.
- Insufficient Attack Protection — log bad requests by source; API gateways for throttling rate-limiting and blocking; layer-7 firewalls for signature-based attack detection.
- CSRF — CSRF tokens for state-changing requests; SameSite cookie attribute; don't use your site as a launchpad for XSS attacks on others.
- Using Components with Known Vulnerabilities — the Equifax breach (Struts 2 CVE); maintain a full dependency tree report; use automated CVE checking in the build pipeline; treat container images as perishable goods (automated builds from upstream base images).
- Underprotected APIs — authorize both on the way out (generating links) and on the way in (receiving requests); TLS with downgrade protection; fuzz-test parsers.
Principle of Least Privilege: no production process should run as root; per-application OS users; containers isolate applications from each other but container patch management is immature (automate builds from upstream base images).
Secret/Password Management: separate from other config files; read-only file permissions owned by the application user; password vaulting (KMS, Vault) — reduces the problem to securing one key; zero tolerance for credentials in version control or process memory; disable core dumps on production systems.
"Pie Crust" Defence (antipattern): authenticating only at the perimeter while services inside trust each other freely. Nygard explicitly names this antipattern — equivalent to zero-trust's rejection of castle-and-moat perimeter security (→ Zero Trust). Internal APIs must also authenticate callers and encrypt traffic.
Security as an Ongoing Process: security decisions (encrypted communication, encryption at rest, authentication, authorization) are cross-cutting architectural concerns, not a one-time review. Must be baked in from the beginning; frameworks cannot protect you automatically from all Top 10 items.
Chapter 10 — Control Plane
The control plane encompasses all software whose job is managing other production software (not handling production user data directly). Every component is optional; adopt based on team size and rate of change, since operational cost scales with complexity.
Mechanical Advantage and System Failure: Automation has no judgment. When it goes wrong, it goes wrong very quickly — faster than humans can perceive and intervene. The AWS S3 outage (Feb 28, 2017) illustrates this: a mistyped command in a playbook removed far more servers than intended because the tool allowed too much capacity to be removed too quickly. Amazon's postmortem does not use the phrase "human error" — deliberately. It was a system failure: the tool, the playbook, and the processes created conditions where a minor mistake had enormous consequences. The same pattern: Reddit's autoscaler shut down the fleet based on partial ZooKeeper data. Lesson: build safeguards into automation tools themselves (rate limiting, minimum capacity floors, confirmation above thresholds).
Two Fundamental Questions for System-Wide Transparency:
- Are users receiving a good experience?
- Is the system creating the economic value we want? "Is everything running?" is not on the list. Partially broken is normal at scale.
Economic Value framing of monitoring: Monitoring is not just a technical concern — it is a financial one.
- Top line (revenue): watch each step of a business process for drop-offs; watch queue depth as first indicator of performance degradation; service exceptions in revenue-generating flows = top-line reduction.
- Bottom line (costs): infrastructure costs (autoscaling unchecked = unexpected bills); operational labour (incident response = unplanned work = opportunity cost); runtime footprint (language/runtime choice affects instance count).
- Transparency tools should answer: are we making what we should? Where are bottlenecks limiting revenue? Where are we overscaled?
What to Expose in Metrics: Traffic indicators; business transactions (counts, value, aging, conversion, completion); users (demographics, usage patterns, errors, login rates); resource pool health (enabled state, total/checked-out/high-water/created/destroyed, threads blocked waiting); database connection health (SQLExceptions, query count, average response time); data consumption (rows/entities, memory/disk footprint); integration point health (circuit breaker state, timeouts, requests, average response time, good/network-error/protocol-error/app-error counts, actual IP of remote endpoint, concurrent requests, high-water mark); cache health (items, memory, hit rate, GC flushes, configured limit, creation time).
Configuration Services (ZooKeeper, etcd): Useful for dynamic, high-rate-of-change environments. Not elastic — adding/removing nodes is disruptive and degrades performance during rebalancing. Critical design rules: (1) instances must start without the config service; (2) instances must continue working when config is unreachable; (3) a partitioned config node must not have the ability to shut down the world; (4) replicate across geographic regions.
Command and Control: Controls to expose to operators: reset circuit breakers; adjust connection pool sizes and timeouts; disable specific outbound integrations; reload configuration; start/stop accepting load; feature toggles. Warning: don't build "flush cache" or "delete all data" controls into production code — these are hazardous and represent a breakdown in roles. Scriptable CLI > GUI for long-term production operations: GUIs can't be scripted, force per-instance manual processes, and slow operators during incidents.
Development Is Production: Dev, CI, and QA environments are the production environment for software creation. Downtime there prevents developers from doing their jobs. Treat them with production-level SLAs.
Chapter 9 — Interconnect
The interconnect layer: load balancing, service discovery, demand control, network routing, and virtual IPs that knit instances into a system.
DNS: Use for stable, slowly-changing infrastructure. Avoid DNS round-robin — clients cache the first address, don't account for health, and can't rebalance under load. Logical service names > physical hostnames. Global Server Load Balancing (GSLB): health-aware DNS servers at each geographic location, returning the address of a healthy local pool; used for disaster recovery and closest-point-of-presence routing.
Load Balancing: Software load balancers (HAProxy, nginx) are reverse proxies at layer 7 — low cost, high flexibility but limited throughput. Hardware load balancers operate layers 4–7, far higher throughput at high cost. Virtual IPs (VIPs) map service names to pools. Key pool config: load-balancing algorithm, health check, stickiness policy, behaviour when no members available. Content-based routing: send different URL paths to different pools (search vs checkout vs admin).
Demand Control: "Every failing system starts with a queue backing up somewhere." Resources form queues: listen queue (TCP backlog), thread queue (one thread per active request), I/O buffers. Under high load, requests stay longer in queues → less headroom for new arrivals → going nonlinear. Shed load at the edge, as early as possible — reject work at the load balancer with 503 when health check fails; a fast rejection is always better than a slow timeout. Keep listen queues short (heuristic: (max_wait_time / mean_processing_time + 1) × thread_count × 1.5).
Service Discovery: Consul (AP — remains available, may serve stale data) vs ZooKeeper/etcd (CP — rejects queries during partition). Clients should cache results. Don't roll your own — correctness in distributed state is much harder than it appears. In highly dynamic environments (containers, cloud), static DNS cannot keep up.
Migratory Virtual IPs: Active/passive failover clusters use a virtual IP that migrates from primary to secondary NIC when the active node fails (via ARP update). Callers must never use the physical hostname; only the VIP's DNS name. Any application calling through a failover VIP must handle IOExceptions and retry (see Circuit Breaker for retry safety limits).
Chapter 8 — Processes on Machines
Design concerns for the individual instance (service = collection of processes; instance = single machine's process; executable = build artifact; process = OS runtime; installation = files on disk; deployment = act of creating an installation).
Code and Supply Chain Security: Chain of custody from dev to production. Never build production artifacts on developer machines (polluted with personal tools). Only CI server writes to the package repository. Private dependency repository with verified digital signatures — downloading from the Internet is convenient but insecure (MITM or compromised upstream). Build-system plugins are also attack vectors (compromised Jenkins plugin example).
Immutable and Disposable Infrastructure: Mutable configuration management (Chef, Puppet) has "layers of stucco" — the state of a machine is the history of all changes applied to it, which can include side effects and partial failures not described by any recipe. Better: always start from a known base image and never patch in place; create a new image for each change. Containers embody this. Disposability > persistence: the ability to throw away and recreate the environment is more valuable than the environment itself.
Configuration:
- Keep per-environment config outside the deployment directory so code image doesn't change per environment.
- Never commit production credentials to source control. Use a separate, locked-down configuration repository.
- In dynamic environments (EC2, containers): inject config at startup (env vars, EC2 user data) or use a configuration service (ZooKeeper/etcd — but this creates a hard dependency with high operational overhead; only worth it at scale).
- Name config properties by function, not type:
authenticationProvidernothostname.
Transparency (Nygard's term, broader than observability): "qualities that allow operators, developers, and business sponsors to gain understanding of historical trends, present conditions, instantaneous state, and future projections." Must be designed in from the beginning — adding it late is like adding quality late. A transparent system matures faster because problems are visible.
Key design rule: monitoring as exoskeleton, not woven in. Alert thresholds, rollup logic, and health status decisions should stay outside the instance in the control plane — they change at a different rate than application code.
Logging:
- Log ERROR only for things that require operator action: circuit breaker tripping to OPEN, database connection failure. User input validation exceptions are WARNING at most.
- No debug logs in production — add a CI step to strip debug-level configs from builds.
- Voodoo operations: ambiguous or misleadingly worded log messages create false temporal correlations that become operational folklore. Example: a debug message "Data channel lifetime limit reached. Reset required" (meaning: the application is about to rotate its own encryption key) coincidentally appeared before a database crash. For six months afterward, operators performed weekly database failovers in response to that message. Root causes: (1) message said "reset required" without specifying the actor; (2) debug logs were left on in production.
- Include request/trace IDs in every log message — essential for post-incident investigation.
- Log interesting state transitions even if also emitting metrics (leaves options open for forensics).
Health Checks:
- Should expose: host IP, runtime/interpreter version, app version or commit ID, whether the instance is currently accepting work, status of connection pools / caches / circuit breakers.
- Used by load balancers for "go live" transition (not just crash detection): when health check transitions from failing to passing, the instance is ready for traffic.
Chapter 7 — Foundations
Infrastructure layer: networking in data centers and cloud, physical hosts, VMs, and containers. The chapter establishes the physical foundation that the rest of "design for production" builds on.
NICs and Names: Hostname ≠ DNS name. Production servers are multihomed (multiple NICs): production traffic, backup, admin, possibly more. Applications that listen on "all interfaces" by default may accept admin connections from the production network or vice versa. Servers must be configured to bind to specific interfaces, not all.
Physical Hosts: Commodity expendable hardware dominates. Design for horizontal scale. Specialised exceptions: high-RAM for graph processing, GPU for embarrassingly parallel workloads.
Virtual Machines: Performance is unpredictable due to oversubscription (a 16-core host may be allocated to 32 vCPUs across VMs). VM clocks are not monotonic — a VM can be suspended and resumed with clock drift, or migrated to a host with different clock. Never trust OS clock for external time; use NTP.
Containers in Data Center: Short-lived identities; no meaningful local storage; networking is the hard problem (VLANs, VXLANs, overlay networks, software switches). Orchestration (Kubernetes, Mesos) needed for scheduling and networking. Container image must not contain credentials or hostnames — inject via environment or configuration service at startup. Target startup time: 1 second.
12-Factor App (Heroku): A checklist for cloud-native deployable applications: (1) single codebase in version control; (2) explicit dependencies; (3) config in environment; (4) backing services as attached resources; (5) separate build/release/run stages; (6) stateless processes; (7) port binding; (8) scale via process model; (9) disposability (fast startup, graceful shutdown); (10) dev/prod parity; (11) logs as event streams; (12) admin tasks as one-off processes.
VMs in Cloud: Individual cloud VMs have worse availability than physical machines (more moving parts, shared host, can be killed by control plane). Ephemeral identities — IP changes on every boot. VMs must "volunteer" for work rather than being assigned it: competing consumers on queues for async work; autoscaling + load balancers for HTTP.
Chapter 6 — Case Study: Phenomenal Cosmic Powers, Itty-Bitty Living Space
A retail online store's Black Friday incident. The chapter is a practical demonstration of several antipatterns from ch. 4 occurring simultaneously in production.
The incident: on Black Friday at peak load, all 3,000 request-handling threads on 100 front-end servers blocked waiting for the order management (back-end) system. CPU usage across all tiers was low — the classic thread starvation signature, not a CPU-bound overload. Thread dumps revealed threads blocked on a resource pool checkout with no timeout (checkoutBlockTime not configured).
The cascade (Unbalanced Capacities antipattern, ch. 4):
- 3,000 front-end threads → overwhelmed order management (450 threads)
- 450 order management threads → overwhelmed scheduling server (~25-concurrent capacity)
- Scheduling server: 3 of 4 servers were offline for holiday maintenance. The remaining server was at 100% CPU.
- Root cause: marketing had published a newspaper insert for "free home delivery" on Black Friday — a Self-Denial Attack the engineering team did not know about.
Alert fatigue: the scheduling server's on-call team had been desensitized by chronic false-positive CPU alerts. The real signal — the server pinned at 100% CPU — was ignored because the on-call team had been trained by false positives to ignore high CPU pages.
Response time as a lagging indicator: response time can only be measured on completed requests. Under thread starvation, requests time out before completing — so reported average response time shows only the fast requests. SiteScope synthetic transactions going red was the real signal.
The accidental Bulkhead that saved the day: the front-end had a separate connection pool for scheduling requests (probably a Conway's Law artifact — a separate team owned scheduling). This became the throttle. By setting that pool's max to zero via live script and recycling the service, scheduling requests were disabled without affecting other front-end traffic. The site recovered within 90 seconds.
Recovery-Oriented Computing (ROC): the ability to restart a single connection pool component (stopService/startService via an admin API) rather than rebooting entire application servers was critical. Full server restart would have taken >6 hours under that load. Dynamic reconfiguration took 5 minutes. ROC principles: (1) failures are inevitable; (2) complete a priori prediction of all failure modes is impossible; (3) human action is a major source of failures. Anticipate them, improve recovery, not just prevention.
Observability as a survival factor: custom Perl scripts screen-scraping the application server's admin HTML GUI for metrics (latency, thread counts, session counts, heap) across all servers simultaneously. The team survived because they had real-time visibility into thread state. The chapter makes the case that operational tooling built during development (not post-incident) is a prerequisite for surviving novel production failures.
Chapter 5 — Stability Patterns
The twelve patterns that stop or slow crack propagation. These are "crackstoppers" — they can be applied independently or in combination.
Timeouts: Every wait must have a limit. Applies to: integration point calls, resource pool checkout (getConnection()), mutex/lock acquisition. Timeouts isolate faults by bounding how long one integration point can consume a thread. A timeout alone doesn't do anything — the caller must handle it. Combine with Circuit Breaker: on timeout, record a fault; when fault density trips the breaker, stop calling. Timeout values should be derived from the dependency's latency distribution (P99.9 sets the false-timeout rate).
Circuit Breaker: Nygard's implementation guidance goes beyond the basic state machine (→ Circuit Breaker). Track fault density (faults per time window) using a leaky bucket rather than an absolute counter — this prevents a burst of old faults from tripping the breaker indefinitely. State changes must be logged and visible to Ops — a breaker tripping silently is a hidden operational event. Scope is per process: circuit breaker state is not shared across instances (each instance tracks its own fault density). Fallback strategies (what to do in OPEN state) require business input — return stale data? Return an error? Show a degraded UI? Engineering alone cannot answer this.
Bulkheads: Physical isolation of resources (→ Bulkhead). Nygard's additions: partition service capacity to reserve a thread pool for critical callers (monitoring, admin, health checks) so that a flood of low-priority requests cannot starve operations that must succeed. Cloud availability zones are a built-in bulkhead at the infrastructure level. CPU binding (pinning processes to CPU cores) is a lower-level isolation technique. The key question is: how much isolation is worth the resource overhead?
Steady State: Avoid human presence on production systems. Every mechanism that accumulates state (data, logs, in-memory caches, metrics) must have a paired mechanism that recycles it. Specific practices: purge old data with application logic (not manual DBA intervention); rotate logs and ship them off the production server (don't let them fill the disk); bound all in-memory caches with LRU or time-based expiration. "Fiddling with production" — developers SSHing in to clear caches or delete rows — is an availability risk. Automate all steady-state maintenance.
Fail Fast: Check resource availability before beginning work on a request — not partway through. If a required Circuit Breaker is OPEN, a required DB connection pool is exhausted, or a required external service is known unavailable, return an error immediately without consuming resources. Nygard calls this "mise en place" — everything in its place before cooking starts. Distinction: Fail Fast applies to incoming requests (fail before doing work); Timeouts apply to outgoing requests (bound how long you wait). Fail Fast also means distinguishing system failures (resource unavailable — return a 503, not a 500) from application failures (bad input — return a 400).
Let It Crash: The cleanest possible application state is immediately after startup. Rather than attempting to catch and recover from every possible error, crash the process and let a supervisor restart it from clean state. Borrowed from Erlang's "let it crash" philosophy. Critical constraints: (1) granularity must be small — a whole JavaEE application server takes minutes to restart; an actor, microservice, or Go container takes milliseconds; (2) replacement must be fast — if not, use fault-tolerant recovery instead; (3) supervision trees must be in place before startup; (4) reintegration must be managed via Circuit Breaker or health checks to avoid re-adding a crashed process before it's ready. Don't use Let It Crash if restart takes minutes (JavaEE) — use fault tolerance instead.
Handshaking: Let a server signal readiness and reject new connections when overloaded, rather than accepting requests it cannot serve. HTTP: return 503 to the load balancer when health check is failing, so the load balancer stops routing. This is an underused pattern — most systems accept connections until they are completely overwhelmed. Circuit Breaker is a stopgap for when handshaking is not available on the downstream service.
Test Harnesses: A separate server (not a mock) that simulates all the out-of-spec failure modes that real integration tests cannot provoke: slow responses, garbled bytes, connection refused mid-stream, RST packets. Configure behaviour by port or request header. This is the foundation for chaos engineering applied to integration points. Mocks verify "does my code handle what I expect?" — test harnesses verify "does my code handle what I can't expect?"
Decoupling Middleware: The architectural choice of synchronous call-and-response (REST, RPC, gRPC) vs. asynchronous message-oriented middleware (Kafka, RabbitMQ, SQS) has structural consequences. Synchronous calls propagate back pressure and cascading failures — a slow downstream slows the caller. Asynchronous middleware decouples in space (caller doesn't need to know the address of the consumer) and time (caller continues immediately; consumer processes when ready). This is an architectural decision with high switching cost — it cannot easily be retrofitted. Choose based on whether the caller legitimately needs a synchronous response to proceed.
Shed Load: When overloaded, refuse new requests rather than accepting them and failing slowly. Return 503 immediately. Signal to the load balancer via health check. The cost of shedding a request is far lower than the cost of degraded service for all concurrent requests. Applied at the service edge — do not let overload propagate inward.
Create Back Pressure: Within a system (across components that share a deployment boundary), use bounded queues to create natural flow control. When a queue is full, the producer blocks — this is the signal to slow down. Unbounded queues mask the backlog: they accept work indefinitely, but response time grows without bound (Little's Law: L = λW). Back pressure is distinct from Shed Load: back pressure applies within a system boundary; Shed Load applies at the boundary.
Governor: Rate-limit automation actions, not just external callers. Automation (autoscalers, config management, deployment systems) can act on incorrect observations and amplify failures faster than humans can respond (→ Reddit ZooKeeper incident, ch. 4). A Governor is stateful and time-aware: it limits the rate of change, not just the magnitude. Critically, it is asymmetric — slow to make large changes, fast to detect alerts. The response curve is U-shaped: act quickly on small, safe changes; slow down and require human confirmation for large changes. The goal is to create an opportunity for human intervention before catastrophe.
Chapter 4 — Stability Antipatterns
The systematic failure modes that appear in production systems. Each antipattern creates, accelerates, or multiplies cracks in the system.
Integration Points: every integration point (database, API, message broker, third-party service) will eventually fail in some way. Failure modes include refused connections, slow responses, hangs, garbled responses, and protocol violations. The failure doesn't arrive cleanly through your defined error handling — it typically manifests as a timeout, a dropped connection, or a malformed response. Defend with Circuit Breaker, Timeouts, Decoupling Middleware, and test harnesses that simulate all failure modes.
Chain Reactions: a load-related defect (memory leak, race condition) exists in every node of a horizontally scaled layer. As nodes fail one by one, the survivors absorb the dead node's share of load. Each failure increases the surviving nodes' probability of failure. The accelerating collapse produces a characteristic pattern: gaps between failures shrink exponentially. The only permanent fix is the underlying defect; Bulkheads can partition a single chain reaction into multiple separate ones that proceed at different rates.
Cascading Failures: failure in one layer triggers failure in a calling layer by propagating through the "gap" between layers. Connection pools are the classic gap — a pool exhausted by blocked threads makes all callers block. Integration points without timeouts are a "surefire way to create cascading failures." Circuit Breaker stops calls to the troubled point; Timeouts ensure callers can return. Retries that don't distinguish transient from permanent errors can amplify cascades.
Blocked Threads: the dominant real-world failure mode. Not a crash — the interpreter is running but every thread is blocked on an impossible outcome. Happens near connection pools, caches, synchronised objects, and integration points. Impossible to exhaustively test for. Defend with: Timeouts on every blocking call; proven concurrency primitives (don't roll your own connection pool or queue); Liskov substitution violations from hidden synchronised keywords; immutable domain objects to avoid synchronization; external monitors with synthetic transactions (not just process/port monitoring).
Self-Denial Attacks: the system conspires against itself — usually via marketing campaigns (email blasts, promotional deep links that bypass CDN). "Limited distribution" deals spread in seconds to millions. One minute before a well-publicized Xbox preorder, a major retailer's site went dark. Mitigation: mass emails in waves, static landing-page "buffer" for the first click, no deep links bypassing CDN/Akamai, no embedded session IDs in URLs, pre-autoscale before known events.
Scaling Effects: point-to-point communication within a cluster scales O(n^2) — fine for two servers, painful for 100. Shared resources become bottlenecks and then SPOFs as the number of callers grows. QA environments (1-2 servers each side) don't reveal O(n^2) effects. Must be designed out (replace P2P with UDP multicast, pub/sub, message queues) — cannot be tested out.
Unbalanced Capacities: front-end capacity can far exceed back-end capacity (3,000 threads calling into 75 threads). A front-end traffic surge (marketing event, celebrity tweet) can overwhelm a back-end that was sized for normal traffic. Defend with: Circuit Breaker on the caller side; Handshaking and Backpressure on the provider side; Bulkheads to reserve capacity for high-priority traffic; stress-test both sides at 10× expected peak.
Dogpile: a synchronised burst of demand when multiple servers restart simultaneously, cron jobs fire on the hour, or caches all expire at once. Requires far more peak capacity than gradual startup would. Defend with: random clock slew (distribute cron jobs, cache TTL jitter); increasing backoff after restarts. Related: Force Multiplier — automation (autoscalers, service discovery) that acts on incorrect beliefs can amplify failures at scale (Reddit ZooKeeper incident: autoscaler read partial ZooKeeper data, decided too many servers were running, shut them all down). Safeguards for automation: rate-limit changes, add hysteresis, alert when >80% of instances appear down (it's probably the observer), confirm large deltas.
Slow Responses: generating a slow response is worse than refusing a connection or returning an error — it ties up resources at both ends. In middle-layer services, slow responses cause upstream callers to also slow down (gradual cascading failure upward through layers). For websites, slow responses cause users to hit reload, adding more load. Memory leaks manifest via slow responses (GC overhead). Defend with self-monitoring: when a moving average of response time exceeds SLA, refuse new connections (Fail Fast) rather than continuing to deliver slow service.
Unbounded Result Sets: query returns far more data than expected — memory exhaustion, long-running loops. Typically invisible in development/QA (small datasets); revealed after months of production accumulation. "Black Monday" example: a JMS message table expected to hold <1,000 rows accumulated 10 million rows; each app server queried all rows, locked them, exhausted memory, crashed, rolled back the lock, allowing the next server to repeat. Always: paginate; use LIMIT/TOP clauses; never trust data producers to limit output; test with production-sized data.
Key Terminology (ch. 3): Fault — incorrect internal state (latent bug triggered, unchecked boundary condition). Error — visibly incorrect behaviour (trading system buys $10B of Pokémon futures). Failure — system does not respond (regardless of whether the process is running). Fault → Error → Failure is how cracks propagate. Tight coupling accelerates crack propagation (events are not independent — failure in one layer increases probability of failure in coupled layers).
Chapter 3 — Stabilize Your System
Philosophical bridge between the case study and the antipatterns catalogue.
Stability definitions: a transaction is an abstract unit of work (not a DB transaction); a system is the complete interdependent set of hardware and software required to process those transactions. A robust system keeps processing transactions through impulses (rapid shocks — flash mobs, celebrity tweets) and stresses (sustained forces — slow credit card processor). Stress causes strain in the system that propagates to unrelated components. A system with longevity keeps running between deployments without degrading.
Longevity bugs (memory leaks, data growth) are invisible in development (app servers rarely run more than minutes) and in QA (short cycles, not production load). The only reliable way to find them before production: longevity tests — a dedicated machine running continuous low-level load for weeks. Leave slack hours in the script to expose connection-pool and firewall timeouts that only manifest after idle periods.
Failure modes are designed, not discovered: systems will crack. Designed failure modes (crumple zones) protect the indispensable parts by absorbing failure elsewhere. Without designed failure modes, cracks propagate unpredictably. The first step: ask of every external call, I/O, resource use, and expected outcome — "what are all the ways this can go wrong?"
Fault-tolerant vs "let it crash": two valid philosophies that agree on the key constraint — keep faults from becoming failures. Either catch and recover, or crash fast and restart from known-good state. The patterns in Chapter 5 implement "crackstoppers" from both camps.
Chapter 2 — Case Study: The Exception That Grounded an Airline
An airline's check-in kiosks and IVR systems fail during peak morning travel, stranding thousands of passengers and generating national news coverage.
Root cause: A JDBC connection pool was exhausted after a database failover. After the failover, every JDBC connection in the pool was connected to a stale IP address (the old database host). These connections would still create Statement objects (the driver checks only its own internal state), but executing the statement — or closing it — would throw a SQLException when network I/O failed. The code's finally block caught the exception from stmt.close() and short-circuited before conn.close(), so the connection was never returned to the pool. After 40 such calls, all 40 pool slots were exhausted; subsequent calls to getConnection() blocked forever.
Cascade: CF (Central Facilities) application servers became fully blocked → CF could not serve any EJB requests → kiosk application servers' 40 threads all blocked waiting for CF response → IVR servers similarly blocked → airline unable to check in passengers.
Key lessons:
- Connections should be validated before use — connection pools should test connections at checkout (or on a background heartbeat) to detect stale connections after failover.
- Resource pools must have timeouts — callers blocking on
getConnection()with no timeout will block forever under pool exhaustion. - Exception handling in cleanup code is easily missed —
finallyblocks that can throw must handle those exceptions explicitly; ifstmt.close()throws,conn.close()must still run. - Monitoring a status URL is not sufficient — the CF monitoring was pinging an HTTP endpoint that was served by a different thread pool than the EJB pool. The system appeared healthy while it was actually fully hung. Deep health checks that exercise real code paths are required.
- One bug can cascade across a whole system — the bug was tiny (one missing try/catch in a finally block) but its impact was enterprise-wide because the systems were tightly coupled without any failure isolation.
Chapter 1 — Living in Production
Foundational chapter establishing the book's philosophy and scope.
"Design for production" vs "design for QA": Most software is designed to pass tests. Tests describe what systems should do. Production tests what systems should not do — crash, hang, lose data, violate privacy, generate runaway costs. The gap between these two perspectives is the gap that causes most production incidents.
Design decisions are financial decisions: Architectural choices determine operational costs over the system lifespan, which is far longer than the development period. The ROI calculation is explicit: investing $50,000 in a zero-downtime deployment pipeline to avoid 5-minute release windows over 5 years of monthly releases (300 minutes × $3,000/minute = $900,000 in downtime cost) returns 18×. Most engineers optimise development cost because that's where their incentives point; the organisation's interests demand optimising operational cost.
The pragmatic architect: Nygard distinguishes two archetypes. The ivory-tower architect aims for end-state perfection — beautiful models, absolute standards, irreversible technology mandates. The pragmatic architect thinks about dynamics: "how do we deploy without a reboot?", "what metrics do we need?", "which part of the system needs the most improvement right now?" The pragmatic architect's system is good enough for today's stresses and clearly identifies which components need replacing as those stresses change.
Early decisions carry disproportionate weight: Team structure is the first draft of architecture (Conway's Law). The earliest decisions — on system boundaries, subsystem decomposition — are the hardest to reverse later; they crystallise into team structure, funding, and programme management. Yet these decisions are made when the team is most ignorant of the eventual structure. The implication: build incrementally, get into production early, let production feedback inform architecture.
Related Pages
- Availability — production-readiness is fundamentally about keeping systems available
- Common Failure Causes — the airline case study illustrates resource exhaustion and cascade failure
- Circuit Breaker — the primary stability pattern; ch. 5 adds leaky bucket, ops visibility, single-process scope
- Bulkhead — isolating failures; ch. 5 adds capacity reservation for critical callers, cloud AZs, CPU binding
- Timeout — ch. 5: every wait must have a limit; sized by P99.9; combined with circuit breaker
- Retry — ch. 5 / ch. 9: retry amplification in chains; retry with backoff; exponential backoff with jitter
- Observability — deep health checks, thread dumps, monitoring (vs Nygard's monitoring-status-page antipattern)
- Deployment Pipelines — zero-downtime deployment addressed in ch. 1 ROI argument
- Messaging — Decoupling Middleware pattern: async vs sync choice is architectural, high switching cost
- Rate Limiting — Governor pattern: rate-limit automation actions, not just callers
- Chaos Engineering — ch. 17; principlesofchaos.org definition; Netflix Simian Army; FIT; blast radius control; ChAP automation platform