Every API team has that one outage story. The one where a downstream service slowed down, and within minutes the entire system collapsed into a pile of retries and timeouts. The postmortem blamed a missing circuit breaker, but the real issue was a collection of small design gaps that together created a cascade. This guide is for backend engineers and architects who want a practical checklist of patterns that make APIs resilient in production — not just in demos.
We focus on patterns that have been battle-tested across many production systems: circuit breakers, retries with backoff, idempotency keys, bulkheads, and rate limiting. For each, we explain the core mechanism, the trade-offs, and the most common mistakes. We also cover when not to use a pattern, because over-engineering resilience can be as dangerous as under-engineering it.
By the end, you'll have a decision framework you can apply to your own APIs, plus a maintenance checklist to keep patterns from drifting into anti-patterns over time.
Where These Patterns Matter Most
Resilience patterns are not one-size-fits-all. The value of a circuit breaker depends on your dependency topology. The cost of idempotency keys depends on your write volume. Before we dive into specifics, it helps to map the landscape where these patterns earn their keep.
High-Traffic Public APIs
If your API serves thousands of requests per second from external clients, every millisecond of latency and every error code gets amplified. A single retry storm from a misconfigured client can saturate your database connection pool. Here, patterns like rate limiting and bulkheads are essential to protect your backend resources. We've seen teams deploy rate limiting at the gateway level with token bucket algorithms, and bulkheads to separate critical read paths from expensive write operations.
Microservice-to-Microservice Calls
Internal service meshes often have dozens of dependencies. A slowdown in one service can propagate quickly if there are no circuit breakers. In one composite scenario, a team had a billing service that called a payment gateway, a fraud detection service, and a notification service. When the fraud service started returning 500s, the billing service kept retrying, which caused its connection pool to exhaust, and then every other caller to the billing service started timing out. A circuit breaker around the fraud call would have isolated the failure.
Third-Party Integrations
When you depend on an external API — a payment provider, a mapping service, a weather data feed — you have no control over its uptime. Idempotency keys become critical to avoid duplicate charges or duplicate orders. Retry with exponential backoff is standard, but without a circuit breaker, a prolonged outage at the third party can still take down your service. We recommend a two-layer approach: retry with backoff for transient failures, and a circuit breaker that opens after a threshold of consecutive failures to give the third party time to recover.
Batch and Event-Driven Systems
Resilience patterns also apply to asynchronous processing. If a message consumer fails to process a message, it might retry indefinitely, causing a backlog. Here, a dead-letter queue (DLQ) acts as a safety net, and a circuit breaker on the consumer side can pause processing when the downstream is unhealthy. Many teams forget that retry patterns need to be bounded — unlimited retries can lead to resource exhaustion.
Foundations That Teams Often Misunderstand
Even experienced engineers sometimes confuse the mechanisms behind these patterns. Let's clarify a few foundational concepts that are frequently misunderstood.
Timeouts Are Not Retries
A timeout is a limit on how long you wait for a response. A retry is a second attempt after a timeout or failure. They work together, but they serve different purposes. A common mistake is setting timeouts too long (e.g., 30 seconds) and then retrying multiple times, which can cause a single request to consume minutes of server resources. A better approach is to set a short timeout (e.g., 2 seconds) and allow a limited number of retries with exponential backoff. The total time budget should be acceptable for the user experience.
Circuit Breakers vs. Retries
Some teams think circuit breakers and retries are alternatives. They are not. A retry is a tactical response to a transient failure. A circuit breaker is a strategic response to a systemic failure. You should use both: retry with backoff for the first few failures, and if failures persist, open the circuit to stop further attempts. The circuit breaker then periodically probes the downstream to see if it has recovered. Without a circuit breaker, retries can become a denial-of-service attack on an already struggling service.
Idempotency Is Not Just for Payments
Many developers associate idempotency keys only with payment APIs, but they are useful for any operation that should be executed exactly once. Think of creating a user, sending an email, or updating an inventory count. If a client retries a request because of a network timeout, the server can use the idempotency key to detect the duplicate and return the original response instead of performing the action again. This pattern requires a data store (usually Redis or a database) to track keys and their responses, with a reasonable expiration (e.g., 24 hours).
Bulkheads Are About Resource Isolation
The term comes from shipbuilding: a ship is divided into watertight compartments so that a breach in one does not flood the entire vessel. In API design, bulkheads mean allocating separate thread pools, connection pools, or even separate service instances for different workloads. For example, a read-heavy endpoint might share a pool with a write-heavy endpoint, and if the write endpoint becomes slow, it can starve the read pool. A bulkhead pattern would give each endpoint its own pool, so a failure in one does not affect the other. This adds operational complexity but can be critical for mixed workloads.
Patterns That Usually Work
Based on what practitioners commonly report, these five patterns have the highest success rate when implemented correctly. We present them with concrete implementation guidance.
Circuit Breaker with Half-Open State
The circuit breaker pattern has three states: closed (normal operation), open (failures exceed threshold, requests are rejected immediately), and half-open (after a timeout, a probe request is allowed to test recovery). The key implementation detail is the half-open state. Many libraries default to a fixed timeout (e.g., 30 seconds) before transitioning to half-open, but that may be too aggressive or too conservative. A better approach is to use an adaptive timeout based on the downstream's typical recovery time. For example, if the downstream has historically recovered within 5 seconds, use a shorter timeout. If recovery takes minutes, use a longer one. Also, track the number of consecutive failures to adjust the threshold dynamically.
Retry with Exponential Backoff and Jitter
Exponential backoff means waiting 1 second, then 2, then 4, then 8, up to a maximum delay. Jitter adds randomness to prevent thundering herd problems when many clients retry simultaneously. A common implementation is to use a base delay of 100ms, multiply by 2 on each retry, and add a random jitter of up to 50% of the current delay. Limit retries to 3–5 attempts. One pitfall: retrying on 4xx errors (client errors) is usually wrong — only retry on 5xx (server errors) or network timeouts. Also, consider the idempotency of the operation: non-idempotent requests should not be retried without an idempotency key.
Idempotency Keys with Idempotency-Key Header
Clients generate a unique key (e.g., a UUID) and include it in the Idempotency-Key header. The server checks if it has seen that key before. If yes, it returns the cached response (including the original status code). If no, it processes the request and stores the key and response. The storage should have a TTL (e.g., 24 hours) to avoid unbounded growth. A common mistake is to store only success responses — if the first attempt failed with a 500, a retry with the same key should be allowed to proceed (the server should not return the cached 500). The key should be scoped to the user or API key to prevent key collision across clients.
Bulkheads with Separate Connection Pools
In a Java application using HikariCP, you can define multiple data sources with different pool sizes. For example, a pool for critical read queries (max 10 connections) and a pool for heavy batch writes (max 5 connections). In a microservice architecture, you might run separate instances for different workloads. The trade-off is increased resource usage — each pool reserves connections even when idle. Monitor pool utilization to right-size the pools. A good starting point is to allocate 80% of connections to the critical path and 20% to non-critical tasks.
Rate Limiting with Token Bucket
Token bucket is a classic algorithm: tokens are added at a fixed rate (e.g., 10 tokens per second) up to a maximum bucket size (e.g., 100 tokens). Each request consumes a token. If no tokens are available, the request is rejected (HTTP 429 Too Many Requests) or queued. This allows short bursts while enforcing a long-term average rate. Implement rate limiting at the API gateway or reverse proxy (e.g., Nginx, Envoy) to offload the logic from application code. Set meaningful error responses with a Retry-After header so clients can back off. A common mistake is to apply the same limit to all endpoints — sensitive endpoints (e.g., login) should have stricter limits.
Anti-Patterns That Cause Teams to Revert
Even well-intentioned resilience patterns can backfire. Here are the anti-patterns we see most often in postmortems.
Retry Storm Without Circuit Breaker
This is the classic cascade. Service A calls Service B, which is slow. Service A retries after a timeout. The retries pile up, causing Service A's thread pool to exhaust. Then Service C, which calls Service A, starts timing out and retrying. Soon the entire system is saturated with retries. The fix is always a circuit breaker on the caller side, plus bounded retries with exponential backoff. Without a circuit breaker, retries become a force multiplier for failure.
Unbounded Retries
Some libraries default to infinite retries. We have seen a team where a background job retried a failed API call every 5 seconds for three days, consuming 50,000 unnecessary requests. Always set a maximum retry count (e.g., 3) and a maximum total time budget (e.g., 30 seconds). For asynchronous tasks, use a dead-letter queue after a few retries.
Shared Connection Pools Across Workloads
When a slow endpoint shares a connection pool with a fast endpoint, the slow endpoint can exhaust the pool, causing the fast endpoint to fail with connection timeout. This is a violation of the bulkhead principle. The fix is to separate pools or use a priority queue. In one scenario, a team had a reporting endpoint that ran a 30-second query, and a health check endpoint that needed a quick response. The reporting query used all available connections, and the health check started failing, causing the load balancer to mark the instance as unhealthy. Separate pools solved it.
Ignoring Client-Side Retry Limits
Even if your server is resilient, clients can still cause harm if they retry aggressively. A mobile app that retries on every network error without backoff can overwhelm your API. Document retry policies for API consumers, and enforce rate limits to protect against misbehaving clients. Some teams implement a client-side circuit breaker in their SDK to prevent the client from hammering the server.
Maintenance, Drift, and Long-Term Costs
Resilience patterns are not set-and-forget. Over time, configurations drift, assumptions change, and patterns can degrade into anti-patterns. Here is a maintenance checklist to prevent that.
Monitor Circuit Breaker State Changes
Track how often each circuit breaker opens and closes. A circuit that opens frequently may indicate a chronic problem with the downstream that needs a different fix (e.g., scaling the downstream, adding caching). A circuit that never opens may be configured too leniently. Set alerts on state transitions, especially transitions to open and half-open.
Review Retry Statistics
Log the number of retries per request and the distribution of retry counts. If many requests require 2 or 3 retries, the downstream may be consistently slow, and you should consider increasing the timeout or adding a circuit breaker. If retries are rare, you might reduce the retry count to save resources.
Audit Idempotency Key Storage
Idempotency key stores can grow unboundedly if TTLs are not enforced. Monitor the size of the key-value store (e.g., Redis memory usage). Also, check that keys are being cleaned up properly. A common drift is that the TTL is extended over time to accommodate slower clients, but then the store grows too large.
Test Bulkhead Isolation
Periodically run chaos experiments where you simulate a slow endpoint in one bulkhead and verify that other bulkheads remain unaffected. For example, use a tool like Chaos Monkey to introduce latency in a specific service and check that the critical path still works. Without testing, you cannot be sure the isolation is effective.
Update Rate Limits as Traffic Patterns Change
Rate limits that were appropriate six months ago may be too restrictive or too permissive now. Review traffic patterns monthly and adjust limits accordingly. Also, consider dynamic rate limiting based on server load — for example, reduce limits when CPU usage is high.
When Not to Use This Approach
Resilience patterns add complexity. There are situations where the cost outweighs the benefit.
Low-Traffic Internal Tools
If your API serves a handful of internal users and the downstream services are on the same network with high reliability, adding a circuit breaker and idempotency keys may be overkill. A simple timeout and a single retry might suffice. The operational overhead of maintaining a circuit breaker library and monitoring its state may not be justified.
Stateless Read-Only Endpoints
For read-only endpoints that can tolerate stale data, caching is often a better resilience mechanism than retries. If the downstream fails, serve from cache. Retries are still useful for transient failures, but a circuit breaker may be unnecessary because you can fall back to cache. Similarly, idempotency is irrelevant for reads.
When the Downstream Is Unrecoverable
If a third-party API is known to return errors for hours at a time, a circuit breaker with a short timeout will just keep opening and closing. In that case, consider a fallback strategy (e.g., serve default data, queue the request for later) rather than retrying. The circuit breaker is still useful to stop retries, but the real solution is a different approach to the dependency.
Prototyping and MVPs
In early stages, speed of development matters more than resilience. Adding circuit breakers and bulkheads can slow down iteration. It is acceptable to skip these patterns initially, but document the technical debt and plan to add them before the system goes to production with real traffic. A simple rule: add resilience patterns when you have at least two downstream dependencies and you expect more than 100 requests per second.
Open Questions and FAQ
Even experienced teams have unresolved questions about these patterns. Here are some common ones.
Should we use a centralized circuit breaker or per-instance?
Per-instance circuit breakers are simpler and avoid a single point of failure, but they can lead to inconsistent behavior across instances (one instance may open while another is still retrying). Centralized circuit breakers (e.g., using Redis) provide consistency but add latency and complexity. For most teams, per-instance is sufficient. Centralized makes sense when you have many instances and want to avoid a thundering herd of probe requests.
How do we handle idempotency for batch requests?
For batch endpoints that accept multiple items, you can generate an idempotency key for the entire batch, or assign a key to each item. The latter is more granular but requires more storage. A common approach is to require the client to provide an idempotency key per item in the batch, and the server processes each item independently. If the batch request fails, the client can retry only the failed items.
What is the right circuit breaker threshold?
There is no universal answer. A common starting point is to open the circuit after 5 consecutive failures in a 10-second window. Adjust based on the downstream's typical failure patterns. For a critical dependency, you might open after 3 failures. For a less critical one, after 10. Monitor false positives — if the circuit opens during a transient blip, the threshold may be too low.
How do we test resilience patterns in CI/CD?
Unit tests can verify the logic of retry and circuit breaker libraries, but integration tests are better. Use a test harness that can inject faults (e.g., a mock server that returns specific errors or delays). Some teams use service virtualization tools like WireMock to simulate downstream failures. Chaos engineering in staging is also valuable but harder to automate in CI.
Should we use a library or build our own?
For most teams, using a well-tested library (e.g., Resilience4j for Java, Polly for .NET, Tenacity for Python) is better than building from scratch. Libraries handle edge cases like thread safety and metric collection. Building your own is only justified if you have very specific requirements that no library meets, or if you want to avoid a dependency.
Summary and Next Experiments
Resilience is not a feature you add once — it is a practice you maintain. The patterns we covered (circuit breakers, retries with backoff, idempotency keys, bulkheads, rate limiting) form a solid foundation, but they require ongoing attention to configuration drift and changing traffic patterns.
Here are three specific experiments to try on your own APIs this week:
- Add a circuit breaker to your most critical downstream call. Start with a threshold of 5 failures in 30 seconds, a half-open timeout of 10 seconds, and monitor the number of requests saved. You may be surprised how often it triggers.
- Audit your retry configuration. Check every retry loop in your codebase. Ensure retries are bounded (max 3), use exponential backoff with jitter, and only retry on 5xx or network errors. Remove any retry on 4xx.
- Implement an idempotency key on a non-payment endpoint. Choose a POST endpoint that creates a resource (e.g., a user or an order). Add an optional
Idempotency-Keyheader, store keys in Redis with a 24-hour TTL, and verify that duplicate requests return the original response.
These small experiments will give you concrete data about where your API is vulnerable and where resilience patterns pay off. Over time, you can expand the checklist to cover more scenarios, but starting with these three will already reduce the risk of the next outage.
Comments (0)
Please sign in to post a comment.
Don't have an account? Create one
No comments yet. Be the first to comment!