Engineering

Designing Resilient APIs That Survive Real Traffic

Idempotency, backpressure, versioning and graceful degradation: the unglamorous design decisions that separate an API that demos well from one that survives production.

Abstract arrangement of connected blocks representing API components

Most APIs are designed for the happy path and discover the unhappy one in production, at 3am, during a traffic spike. Resilience is not a feature you add later; it is a set of decisions you make in the first design review. Here are the ones that matter most.

Idempotency is not optional

The network will retry your requests whether you plan for it or not. Clients time out and retry, load balancers replay, users double-click. If a POST /payments can run twice, eventually it will.

The mechanism is simple: the client generates a unique key, the server records the result against that key, and replays return the stored result instead of re-executing.

idempotency.ts ts
async function createPayment(req: PaymentRequest, key: string) {
  const existing = await store.get(key);
  if (existing) return existing; // replay: return the original result

  const result = await processPayment(req);
  await store.put(key, result, { ttl: '24h' });
  return result;
}

The subtlety is the race: two concurrent requests with the same key. Use a conditional write (INSERT ... ON CONFLICT DO NOTHING) so exactly one wins, and have the loser wait for the winner’s result.

Backpressure beats buffering

When a service is overwhelmed, the instinct is to buffer: queue the excess and work through it. This feels resilient and is quietly catastrophic. Unbounded queues convert a latency problem into a memory problem, and a memory problem into an outage.

The healthier response is to reject early and clearly. A fast 429 Too Many Requests with a Retry-After header lets clients back off gracefully. A silent 30-second hang does not.

Set a concurrency limit

Cap in-flight requests per instance based on measured capacity, not hope.

Bound every queue

A full bounded queue is a signal. An unbounded queue is a time bomb.

Shed load explicitly

Return 429 with backoff guidance rather than degrading everyone equally.

Version at the boundary, not everywhere

API versioning terrifies teams because they imagine maintaining every version forever. The trick is to keep versioning at the edge and normalize to a single internal representation immediately.

Diagram of API versioning at the boundary
Translate at the edge; keep one internal model. Old versions become thin adapters.

Your business logic should never contain an if (version === 'v1') branch. Adapters translate inbound requests to the canonical shape and outbound responses back to the requested version. When you retire a version, you delete an adapter, not perform surgery through the core.

Design for graceful degradation

The goal is not “never fail.” It is “fail small.” A product page that loads without its recommendation carousel is a minor annoyance; one that returns a 500 because the recommendation service is slow is an outage.

DependencyCriticalityStrategy on failure
Auth serviceCriticalFail closed, return 503
Product catalogCriticalServe cached, flag staleness
RecommendationsOptionalOmit section, log, move on
Analytics beaconCosmeticFire-and-forget, never block
Degradation strategies by dependency criticality.

Classify every dependency by criticality before you wire it in. The optional ones should be behind timeouts short enough that their failure is invisible to the user.

The most reliable systems are not the ones with the fewest failures. They are the ones where failures stay contained.

The checklist

Before an API goes live, I want yes answers to all of these:

  • Can every write be safely retried?
  • Is every queue and buffer bounded?
  • Does every outbound call have a timeout and a fallback?
  • Can we deploy a breaking change without a client rewrite?
  • When a dependency dies, does the blast radius stay local?

None of this is exotic. It is a collection of small, deliberate decisions that compound. Make them early, and the 3am page never comes.

Continue reading