Building Bulletproof APIs: Circuit Breaker, Rate Limiting, and Graceful Degradation


When your API suddenly gets 50,000 requests per second and you're wondering if this is how your tech career ends... ๐
Introduction
As developers, we've all been there, you deploy your beautiful API, everything works perfectly in testing, and then real users show up. Suddenly your database is crying, your servers are on fire, and you're getting calls at 3 AM because the entire system crashed.
The thing is, building APIs that can handle real-world traffic isn't just about writing clean code. It's about understanding that failures will happen, and designing your system to handle them gracefully. This article breaks down three essential patterns that separate amateur APIs from production-ready systems.
What Makes APIs Fail Under Pressure?
Before we jump into solutions, let's understand the enemy. APIs fail under pressure for three main reasons:
The Cascade Effect
One service fails, and like dominoes, everything else starts falling. Your payment service goes down, your app keeps trying to connect, uses up all its resources, and crashes completely. Now users can't even browse products because you're still trying to process payments.
Resource Exhaustion
Success becomes your biggest problem. Your app gets featured somewhere, traffic spikes 10x, and your server that comfortably handles 100 requests per second suddenly gets 1,000. Memory runs out, connections max out, everything grinds to a halt.
All-or-Nothing Failures
Your recommendation engine has a slight hiccup, and instead of showing products without recommendations, your entire product page throws an error. Users can't buy anything because of one non-essential feature.
These aren't edge cases - they're inevitable realities of running software in production.
The Three Core Reliability Patterns
Circuit Breaker: Stop the Bleeding
Think of circuit breakers like the electrical breakers in your house. When there's too much current (too many failures), the breaker trips to prevent a fire (system crash).
How it works:
Monitor requests to external services
When failure rate crosses a threshold (say 50% in 30 seconds), "open" the circuit
Stop sending requests to the failing service immediately
After a timeout period, try a few test requests
If they succeed, "close" the circuit and resume normal operation
The magic: Instead of wasting resources on requests that will fail anyway, you fail fast and give the struggling service time to recover.
Real-world example: Your e-commerce API depends on a payment processor. The payment service starts responding slowly due to high load. Without a circuit breaker, your API keeps sending payment requests, each taking 30 seconds to timeout. With a circuit breaker, after detecting the pattern, you immediately return "payment temporarily unavailable" and let users save items to cart instead.
class CircuitBreaker {
constructor(threshold = 5, timeout = 60000) {
this.failureThreshold = threshold;
this.timeout = timeout;
this.failureCount = 0;
this.lastFailureTime = null;
this.state = 'CLOSED'; // CLOSED, OPEN, HALF_OPEN
}
async call(operation) {
if (this.state === 'OPEN') {
if (Date.now() - this.lastFailureTime < this.timeout) {
throw new Error('Circuit breaker is OPEN');
}
this.state = 'HALF_OPEN';
}
try {
const result = await operation();
this.onSuccess();
return result;
} catch (error) {
this.onFailure();
throw error;
}
}
onSuccess() {
this.failureCount = 0;
this.state = 'CLOSED';
}
onFailure() {
this.failureCount++;
this.lastFailureTime = Date.now();
if (this.failureCount >= this.failureThreshold) {
this.state = 'OPEN';
}
}
}
Rate Limiting: Control the Flow
Rate limiting is like having a bouncer at a club - you control how many people can enter and how fast, ensuring everyone inside has a good experience.
Why you need it:
Prevent abuse and DoS attacks
Ensure fair resource distribution
Protect your infrastructure from being overwhelmed
Maintain service quality for all users
Common algorithms:
Token Bucket: Users get tokens at a fixed rate. Each request consumes a token. No tokens = wait.
Good for: Allowing burst traffic while maintaining average rate
Example: 1000 requests per hour, but can burst up to 100 in a minute
Sliding Window: Track requests in a rolling time window.
Good for: Precise rate control without burst allowances
Example: Exactly 1000 requests per hour, no more
Fixed Window: Simple counters that reset at fixed intervals.
Good for: Easy implementation, low memory usage
Example: 1000 requests per hour, counter resets at the top of each hour
// Redis-based sliding window rate limiter
class RateLimiter {
constructor(redisClient, limit = 1000, windowMs = 3600000) {
this.redis = redisClient;
this.limit = limit;
this.windowMs = windowMs;
}
async isAllowed(userId) {
const now = Date.now();
const windowStart = now - this.windowMs;
const key = `rate_limit:${userId}`;
// Remove old entries and count current requests
const pipeline = this.redis.pipeline();
pipeline.zremrangebyscore(key, 0, windowStart);
pipeline.zcard(key);
pipeline.zadd(key, now, `${now}-${Math.random()}`);
pipeline.expire(key, Math.ceil(this.windowMs / 1000));
const results = await pipeline.exec();
const currentCount = results[1][1];
return currentCount < this.limit;
}
}
Graceful Degradation: Keep the Show Running
When things start breaking, graceful degradation ensures your core functionality stays available even if some features don't work perfectly.
The philosophy: Partial service is infinitely better than no service.
Strategies:
Feature toggles: Disable non-essential features during high load
Fallback responses: Return cached or default data when real-time data isn't available
Progressive enhancement: Build core functionality first, add nice-to-haves on top
Example scenario: Your social media API depends on multiple services:
User service (essential)
Content service (essential)
Recommendation service (nice-to-have)
Analytics service (nice-to-have)
When recommendation service fails, instead of breaking the entire feed, you show posts without personalized recommendations. When analytics fails, you still track basic metrics locally.
class GracefulDegradationHandler {
constructor(cache, healthChecker) {
this.cache = cache;
this.healthChecker = healthChecker;
}
async getUserFeed(userId) {
const coreData = await this.getCoreData(userId);
// Try to enhance with additional features
const enhancedData = await this.enhanceData(coreData, userId);
return enhancedData;
}
async getCoreData(userId) {
// Essential data - must not fail
try {
return await this.userService.getPosts(userId);
} catch (error) {
// Return cached data as last resort
return await this.cache.get(`posts:${userId}`) || { posts: [], message: "Content temporarily unavailable" };
}
}
async enhanceData(coreData, userId) {
const enhancements = {};
// Non-essential enhancements - fail silently
try {
if (await this.healthChecker.isHealthy('recommendations')) {
enhancements.recommendations = await this.recommendationService.getRecommendations(userId);
}
} catch (error) {
// Continue without recommendations
}
try {
if (await this.healthChecker.isHealthy('analytics')) {
await this.analyticsService.trackView(userId);
}
} catch (error) {
// Analytics failure doesn't affect user experience
}
return { ...coreData, ...enhancements };
}
}
How They Work Together
These patterns aren't meant to work in isolation. In a production system, they complement each other:
Rate limiting prevents your system from being overwhelmed in the first place
Circuit breakers stop cascading failures when dependencies fail
Graceful degradation ensures users can still accomplish their goals
Real scenario: Your API gets featured in a major blog post:
10:00 AM: Traffic starts increasing
10:05 AM: Rate limiting kicks in, controlling the flow
10:15 AM: Despite rate limiting, your database starts struggling
10:17 AM: Circuit breaker opens for heavy database operations
10:18 AM: Graceful degradation serves cached data for non-essential features
10:30 AM: Database recovers, circuit breaker closes, full functionality returns
Users experienced slightly slower responses and some features were temporarily limited, but the core service never went down.
Implementation Considerations
Start Simple
Don't try to implement all patterns at once. Start with basic rate limiting, then add circuit breakers for your most critical dependencies, then implement graceful degradation for non-essential features.
Monitor Everything
These patterns are only effective if you can observe them working. Track:
Circuit breaker state changes
Rate limit hit rates
Degraded vs full functionality usage
Recovery times
Test Failure Scenarios
Use chaos engineering principles to test your patterns :
Randomly kill dependencies
Inject artificial delays
Simulate traffic spikes
Test during actual load
Cultural Shift
Building reliable systems isn't just about code - it's about changing how your team thinks about failure. Instead of "how do we prevent all failures?" ask "how do we control what happens when things fail?"
Common Mistakes to Avoid
Circuit Breaker Pitfalls
Setting thresholds too low (opens on minor hiccups)
Setting timeouts too short (doesn't give services time to recover)
Not implementing proper health checks
Rate Limiting Issues
Being too restrictive (legitimate users get blocked)
Not providing simple error messages about limits
Using the same limits for all types of operations
Degradation Problems
Not clearly defining what's essential vs nice-to-have
Degrading too aggressively (users notice immediately)
Not communicating degraded state to users
Conclusion
Building bulletproof APIs isn't about preventing all failures - it's about controlling how your system behaves when failures inevitably occur. Circuit breakers prevent cascading failures, rate limiting protects your resources, and graceful degradation keeps your core functionality available.
These patterns have saved countless systems from complete outages and turned potential disasters into minor inconveniences. The key is to implement them gradually, monitor their effectiveness, and continuously refine based on real-world usage patterns.
Your users won't remember the day your recommendations were a bit slow, but they'll definitely remember the day they couldn't access your service at all. Build accordingly.
Next up: We'll dive deep into monitoring and observability patterns to ensure you know exactly how your reliability patterns are performing in production.
Subscribe to my newsletter
Read articles from Abigeal Afolabi directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by

Abigeal Afolabi
Abigeal Afolabi
๐ Software Engineer by day, SRE magician by night! โจ Tech enthusiast with an insatiable curiosity for data. ๐ Harvard CS50 Undergrad igniting my passion for code. Currently delving into the MERN stack โ because who doesn't love crafting seamless experiences from front to back? Join me on this exhilarating journey of embracing technology, penning insightful tech chronicles, and unraveling the mysteries of data! ๐๐ง Let's build, let's write, let's explore โ all aboard the tech express! ๐๐ #CodeAndCuriosity