AWS Builder Center: Learn, Build and Connect with builders in the AWS community
AWS Builder Center is the official home for builders on AWS. Share and read what others are working on, follow people who inspire you, explore training and workshops, and find tools to support what you're building.
Implementing a Circuit Breaker Pattern for MCP Service Resilience
If you're building an application that calls MCP (Model Context Protocol) servers — say, an AI-powered support portal or an agentic workflow — you've probably hit HTTP 429 errors when traffic spikes. This article walks through how to add a circuit breaker so your app stays functional even when the MCP server is throttling you.
The Problem
Here's what happens without protection: your app sends requests to an MCP server, the server hits its rate limit and starts returning 429s, your app retries, the retries pile up, and now you've got a cascade. Users see timeouts or blank screens. The MCP server is drowning in retry traffic. Everyone loses.
You'll recognize this pattern if you've seen:
- Sudden HTTP 429 responses from MCP tool calls during peak hours
- Response times jumping from milliseconds to full timeouts
- AI features going dark for end users
- Your retry logic making the problem worse, not better
Why Retries Alone Don't Fix This
Most teams start with exponential backoff. It's fine for the occasional blip — a network hiccup, a momentary spike. But under sustained throttling, retries have three problems:
- They still hold threads — your app is burning resources waiting during backoff delays
- They pile up — when the MCP server recovers, every backed-up retry fires at once (the "thundering herd")
- They offer no alternative — while you're retrying, your user is staring at a spinner
You need a way to fail fast and do something useful instead.
How a Circuit Breaker Helps
The idea is borrowed from electrical engineering. A circuit breaker sits between your app and the MCP server. It watches for failures, and when things go bad, it trips — cutting off requests before they reach the struggling server.
It has three states:
CLOSED — Business as usual. Requests flow through. The breaker counts failures quietly in the background.
OPEN — Too many failures in a short window (say, 5 in 30 seconds). The breaker stops sending requests entirely. Instead, your app immediately returns a fallback — a cached answer, a "try again shortly" message, whatever makes sense. This gives the MCP server room to recover.
HALF-OPEN — After a cooldown (say, 60 seconds), the breaker lets one request through as a test. If it succeeds, great — back to CLOSED. If it fails, back to OPEN for another cooldown.
The Code
Here's a straightforward Python implementation example:
import time from enum import Enum from threading import Lock class CircuitState(Enum): CLOSED = "closed" OPEN = "open" HALF_OPEN = "half_open" class CircuitBreaker: def __init__( self, failure_threshold=5, recovery_timeout=60, monitoring_window=30 ): self.failure_threshold = failure_threshold self.recovery_timeout = recovery_timeout self.monitoring_window = monitoring_window self.state = CircuitState.CLOSED self.failures = [] self.last_failure_time = None self.lock = Lock() def _clean_old_failures(self): """Remove failures outside the monitoring window.""" now = time.time() self.failures = [ f for f in self.failures if now - f < self.monitoring_window ] def can_execute(self): """Check if a request should be attempted.""" with self.lock: if self.state == CircuitState.CLOSED: return True elif self.state == CircuitState.OPEN: if time.time() - self.last_failure_time >= self.recovery_timeout: self.state = CircuitState.HALF_OPEN return True return False elif self.state == CircuitState.HALF_OPEN: return True def record_success(self): """Record a successful call.""" with self.lock: self.state = CircuitState.CLOSED self.failures = [] def record_failure(self): """Record a failed call and potentially trip the circuit.""" with self.lock: now = time.time() self.failures.append(now) self.last_failure_time = now self._clean_old_failures() if len(self.failures) >= self.failure_threshold: self.state = CircuitState.OPEN if self.state == CircuitState.HALF_OPEN: self.state = CircuitState.OPEN
Wiring It Into Your MCP Calls
Now wrap your MCP calls with the breaker. The key addition: when the circuit is open, serve a fallback instead of waiting.
import json mcp_circuit = CircuitBreaker( failure_threshold=5, recovery_timeout=60, monitoring_window=30 ) response_cache = {} def call_mcp_tool(tool_name, parameters): """Call an MCP tool with circuit breaker protection.""" if not mcp_circuit.can_execute(): return get_fallback_response(tool_name, parameters) try: response = invoke_mcp_server(tool_name, parameters) if response.status_code == 200: mcp_circuit.record_success() cache_key = f"{tool_name}:{json.dumps(parameters, sort_keys=True)}" response_cache[cache_key] = { "data": response.json(), "timestamp": time.time() } return response.json() elif response.status_code == 429: mcp_circuit.record_failure() return get_fallback_response(tool_name, parameters) else: mcp_circuit.record_failure() return {"error": "Service temporarily unavailable"} except (ConnectionError, TimeoutError): mcp_circuit.record_failure() return get_fallback_response(tool_name, parameters) def get_fallback_response(tool_name, parameters): """Return something useful when the MCP server is unavailable.""" cache_key = f"{tool_name}:{json.dumps(parameters, sort_keys=True)}" # Try cached response first (5-minute freshness) cached = response_cache.get(cache_key) if cached and (time.time() - cached["timestamp"] < 300): return { **cached["data"], "_fallback": True, "_cache_age_seconds": int(time.time() - cached["timestamp"]) } # Otherwise, tell the user what's happening return { "message": "AI-assisted response temporarily unavailable. " "Please try again shortly or use manual search.", "_fallback": True, "_circuit_state": "open" }
What to Show Users When the Circuit Is Open
Don't just show an error. Pick a fallback that matches the feature's importance:
| Strategy | Good for | What it looks like |
|---|---|---|
| Cached response | Answers that don't change often | Last-known-good answer with a "cached" label |
| Static content | Critical paths that must always respond | Pre-written FAQ or documentation links |
| Feature toggle | Nice-to-have AI features | Hide the AI chat, show regular search |
| Queue it | Requests that aren't time-sensitive | "We'll process this shortly" with async delivery |
The right choice depends on your users. A support portal might show cached KB articles. A chatbot might fall back to keyword search. An analytics tool might queue the request and email results later.
Monitoring: Know When the Circuit Trips
You want to know when this happens in production. Push the circuit state to CloudWatch:
import boto3 cloudwatch = boto3.client("cloudwatch") def publish_circuit_metric(circuit_breaker, tool_name): """Publish circuit state to CloudWatch for alerting.""" state_value = { CircuitState.CLOSED: 0, CircuitState.HALF_OPEN: 1, CircuitState.OPEN: 2 } cloudwatch.put_metric_data( Namespace="MyApp/MCPResilience", MetricData=[ { "MetricName": "CircuitState", "Dimensions": [ {"Name": "ToolName", "Value": tool_name} ], "Value": state_value[circuit_breaker.state], "Unit": "None" }, { "MetricName": "FallbackResponses", "Dimensions": [ {"Name": "ToolName", "Value": tool_name} ], "Value": 1 if circuit_breaker.state != CircuitState.CLOSED else 0, "Unit": "Count" } ] )
Set an alarm on CircuitState >= 2 (OPEN). When it fires, you know your MCP dependency is struggling and users are getting fallback responses.
Bonus: Use Rate Limit Headers to Avoid Tripping at All
AWS MCP servers may include rate limit info in response headers:
def parse_rate_limit_headers(response): """Check how close you are to the limit.""" return { "limit": int(response.headers.get("X-RateLimit-Limit", 0)), "remaining": int(response.headers.get("X-RateLimit-Remaining", 0)), "reset": int(response.headers.get("X-RateLimit-Reset", 0)) }
If X-RateLimit-Remaining is getting low, slow down your request rate before you hit a 429. This is adaptive rate limiting — it prevents the circuit from tripping in the first place. Think of it as the early warning system that keeps the breaker from ever needing to fire.
Architecture at a Glance
Workflow Steps:
- Client sends request to the Circuit Breaker
- Circuit Breaker checks state (CLOSED, OPEN, or HALF-OPEN)
- If CLOSED or HALF-OPEN → forward request to MCP Server
- MCP Server responds — success (200) or throttled (429). Circuit Breaker records the result: success resets the failure count and caches the response; failure increments the count and trips to OPEN if threshold exceeded
- If OPEN → Fallback Handler returns a cached response or degraded message directly to Client without calling MCP Server
- Circuit Breaker publishes state metrics to CloudWatch (alerts when circuit opens)
- After recovery timeout → circuit transitions to HALF-OPEN and allows one test request through
Wrapping Up
Five things to remember:
- Retries alone won't save you — they make sustained throttling worse. A circuit breaker fails fast and protects both sides.
- Always have a fallback — cached data, static content, or a graceful "try again shortly" message. Something is better than a timeout.
- Monitor the circuit — a CloudWatch alarm on circuit state tells you when users are getting degraded responses.
- Check rate limit headers first — if the server tells you how close you are to the limit, use that info to slow down before you get throttled.
- Tune your thresholds — start with 5 failures in 30 seconds, then adjust based on your traffic and the MCP server's actual limits.
Related Resources
Relevant content
asked a year ago
