Resilience Is Triage, Not a Checklist

Software Engineering
Resilience Is Triage, Not a Checklist
Yannic Scheef
Yannic Scheef 25 Jul 2025

Reading length Full · 7 min read
TL;DRShortMediumFull

Every SaaS platform fails eventually. A database runs out of connections, a payment provider has a bad afternoon, a deploy ships a bug. Resilience is not the absence of failure: it is the property of staying useful while parts of the system are broken, and recovering without drama when they break badly.

For a small engineering team, the hard part is knowing which resilience work pays for itself and which is expensive theatre. The honest principle underneath everything below is to match effort to consequence — spend where failure is both likely and painful, and stop before you build complexity you cannot operate. The rest of this piece is what that looks like in practice.

Resilience is not the absence of failure. It is staying useful while parts of the system are broken, and recovering without drama when they break badly.

Graceful degradation beats brittle perfection

The most valuable resilience pattern is also the cheapest: decide, in advance, what your application does when a dependency is unavailable. Most systems do something accidental and bad. They hang on a slow call, exhaust their thread pool waiting, and turn one failed dependency into a total outage.

The fix is to treat every external call as something that can fail, and to define the fallback explicitly:

  • Timeouts on everything. A call with no timeout is a call that can hang forever. Every database query, HTTP request, and cache lookup needs an upper bound measured in seconds, not minutes.
  • Circuit breakers for repeat offenders. When a dependency starts failing, stop hammering it. Fail fast for a cool-down window, then probe before resuming. This protects both you and the struggling service.
  • Defined fallbacks per feature. If the recommendation engine is down, show a default list. If search is degraded, fall back to a simpler query. If the analytics pipeline is offline, queue events and keep serving requests.
  • Idempotency on critical retries. For payments and other money-moving calls, idempotency is non-negotiable: a retried charge must never become a double charge. Build a stable request key in from the start and capture enough state to reconcile once the provider recovers.

A circuit breaker in a dozen lines

The mechanics are simpler than the name suggests. Count consecutive failures; once they cross a threshold, short-circuit calls for a cool-down window instead of waiting on a dependency you already know is sick:

async function callWithBreaker(fn) {
  if (breaker.open && now() - breaker.trippedAt < COOLDOWN_MS) {
    return fallback();           // fail fast, don't pile on
  }
  try {
    const result = await withTimeout(fn, 2000);
    breaker.failures = 0;        // success resets the count
    breaker.open = false;
    return result;
  } catch (err) {
    if (++breaker.failures >= THRESHOLD) {
      breaker.open = true;
      breaker.trippedAt = now();
    }
    return fallback();
  }
}

The discipline is to rank features by how essential they are. Login and core workflows must survive almost anything. A "people also viewed" widget should disappear quietly rather than take a page down with it. A platform that loses its sidebar but keeps processing orders is having a much better day than one that returns a blank error to everyone.

Core (must survive) login checkout order processing Degrade gracefully search → simpler query analytics → queue recs → default list Optional (drop) "also viewed" badges decorative widgets
Rank every feature by blast radius: protect the core, degrade the middle, drop the rest.

Common failure modes and what to do about them

Most production incidents are variations on a handful of recurring shapes. Naming them in advance turns a panicked 3 a.m. scramble into running a playbook you already wrote:

Failure modePrimary mitigationFallback behaviour
Third-party API outageTimeout + circuit breakerServe cached/default response, queue writes for replay
Database unavailableManaged failover replica, connection pool limitsRead-only mode or maintenance page on core paths
Traffic spikeAutoscaling + rate limiting + load sheddingShed low-priority traffic, protect checkout and auth
Bad deployHealth checks + automated rollback, canary releaseRoll back to last known-good version automatically
Payment provider hiccupIdempotent retries with backoffRecord intent, reconcile once provider recovers

You cannot fix what you cannot see

Observability is the difference between a five-minute incident and a two-hour one. The question to optimise for is brutally practical: when something breaks at 3 a.m., how fast can one engineer figure out what and why?

Three layers cover most of the need. Structured logs with a request ID threaded through every service let you reconstruct a single user's journey across components. Metrics such as request rate, error rate, latency percentiles, and saturation of pools and queues tell you the shape of a problem at a glance. Traces show where time goes inside a slow request, which is the only honest way to find the bottleneck in a chain of calls. Google's Site Reliability Engineering books are the canonical free reference for turning these signals into operational practice.

Watch the tail, not the average

Watch the 95th and 99th percentile latency, not the average. Averages hide the tail, and the tail is exactly where users feel pain and where cascading failures begin. A median of 80ms with a p99 of nine seconds is a system with a serious problem the average will never reveal.

Alerting deserves the same care as the code. Alert on symptoms users feel, such as rising error rates, climbing latency, or a queue that stops draining, not on every CPU spike. An alert that fires constantly trains people to ignore it, and an ignored alert is worse than none because it carries false confidence.

Backups, recovery, and the redundancy worth paying for

A backup you have never restored is a hypothesis, not a safety net. The single highest-leverage exercise a small team can run is a periodic restore drill: take a recent backup, restore it into a clean environment, and confirm the data is intact and the application starts. Teams discover their backups are corrupt, incomplete, or missing a critical table only when they finally try to use them, usually during the worst hour of the year.

A backup you have never restored is a hypothesis, not a safety net.

Define two numbers explicitly and make them a business decision, not a technical accident:

  1. Recovery Point Objective (RPO) — how much data you can afford to lose. Each tighter RPO costs more: hourly snapshots are cheap and mean up to an hour of lost work; continuous replication brings loss down to seconds and is not cheap.
  2. Recovery Time Objective (RTO) — how long you can be down while you restore. A four-hour restore is fine for some businesses and fatal for others.

Redundancy follows from those numbers. Run your application across more than one instance so a single crash does not take you offline, and put a managed database with automated failover under it. Keeping application instances stateless — config in the environment, no local session state, as the Twelve-Factor App methodology argues — is what makes that horizontal redundancy actually work: any instance can die and any other can serve the next request. That covers the overwhelming majority of real-world failures: a process dies, a machine reboots, a zone has a bad day.

Be honest about where the gains flatten. Full multi-region active-active deployment is genuinely hard, with data replication lag, split-brain risk, and a testing burden that never ends. For most SaaS products it is the wrong investment. The reliability pillar of the AWS Well-Architected Framework makes the same point in detail: match recovery strategy to the workload's actual criticality. A single region with solid backups, multi-instance redundancy, and database failover is a far better use of a small team's time than a multi-region setup nobody fully understands and therefore cannot operate under stress.

The takeaway: spend where failure hurts

Resilience is not a checklist you complete once. It is a continuous judgement about where failure hurts most and where your time is best spent. The trap for small teams is gold-plating: pouring weeks into exotic failover for a feature whose outage costs little, while the database has never once been test-restored.

Start with the cheap, high-leverage work:

  • Timeouts everywhere — no unbounded calls.
  • Graceful degradation of non-essential features.
  • Idempotency on the calls that move money.
  • Logs and metrics that make incidents legible.
  • A backup you have actually restored.

Add redundancy where a single failure genuinely takes you down, and stop before you build complexity you cannot operate. A good engineering partner can tell you which three things to fix this week and which three to safely defer for a year — and will defend the deferral as firmly as the fix.