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

Abigeal AfolabiAbigeal Afolabi
7 min read

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:

  1. Rate limiting prevents your system from being overwhelmed in the first place

  2. Circuit breakers stop cascading failures when dependencies fail

  3. 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.

5
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