Skip to content

GraphQL Circuit Breaker — Resilience Patterns for GraphQL APIs and Resolvers

DodaTech Updated 2026-06-28 6 min read

In this tutorial, you will learn about Graphql Circuit Breaker. We cover key concepts, practical examples, and best practices to help you master this topic.

GraphQL circuit breaker patterns protect individual resolvers from downstream service failures, enabling partial query results where failing fields return null or fallback data while successful fields continue to respond normally.

flowchart TD
    Q[GraphQL Query] --> Resolver1{Resolver: Products}
    Resolver1 --> CB1{Circuit State}
    CB1 -->|Closed| ProductService[Product Service]
    CB1 -->|Open| ProductNull[Return null]
    Q --> Resolver2{Resolver: Reviews}
    Resolver2 --> CB2{Circuit State}
    CB2 -->|Closed| ReviewService[Review Service]
    CB2 -->|Open| ReviewNull[Return []]
    Q --> Resolver3{Resolver: User}
    Resolver3 --> CB3{Circuit State}
    CB3 -->|Closed| UserService[User Service]
    CB3 -->|Open| UserCache[Return Cached]

What You'll Learn

  • Per-resolver circuit breaker patterns
  • Partial query results
  • Resolver-level fallbacks
  • DataLoader circuit breaker
  • Batching and protection

Why It Matters

A single slow resolver in GraphQL delays the entire response. Circuit breakers at the resolver level ensure that a failing downstream service only affects its resolver's fields, while the rest of the query completes normally.

Real-World Use

DodaTech's GraphQL gateway uses per-resolver circuit breakers for 15 data sources. When the reviews service fails, the reviews field returns an empty array while product details and pricing load normally. Users see products without reviews instead of a blank page.

Resolver Circuit Breaker

import time
import random

class ResolverCircuitBreaker:
    def __init__(self, name, fail_threshold=5, recovery_timeout=30):
        self.name = name
        self.fail_threshold = fail_threshold
        self.recovery_timeout = recovery_timeout
        self.failures = 0
        self.state = 'CLOSED'
        self.last_failure = 0

    def resolve(self, resolver_fn, fallback_value=None, *args, **kwargs):
        if self.state == 'OPEN':
            if time.time() - self.last_failure > self.recovery_timeout:
                self.state = 'HALF_OPEN'
            else:
                return fallback_value

        try:
            result = resolver_fn(*args, **kwargs)
            self.failures = 0
            if self.state == 'HALF_OPEN':
                self.state = 'CLOSED'
            return result
        except Exception:
            self.failures += 1
            self.last_failure = time.time()
            if self.failures >= self.fail_threshold:
                self.state = 'OPEN'
            return fallback_value

product_cb = ResolverCircuitBreaker("product-service")
review_cb = ResolverCircuitBreaker("review-service")
user_cb = ResolverCircuitBreaker("user-service")

def get_products():
    return [{"id": 1, "name": "Laptop"}, {"id": 2, "name": "Phone"}]

def get_reviews():
    raise ConnectionError("Reviews service down")

def get_user():
    return {"id": 1, "name": "Alice"}

query_result = {
    "products": product_cb.resolve(get_products, fallback_value=[]),
    "reviews": review_cb.resolve(get_reviews, fallback_value=[]),
    "user": user_cb.resolve(get_user, fallback_value=None),
}

print(f"Partial query result:")
for key, value in query_result.items():
    print(f"  {key}: {value}")

Expected output:

Partial query result:
  products: [{'id': 1, 'name': 'Laptop'}, {'id': 2, 'name': 'Phone'}]
  reviews: []
  user: {'id': 1, 'name': 'Alice'}

DataLoader Circuit Breaker

import time
from collections import defaultdict

class DataLoaderCircuitBreaker:
    def __init__(self, name, batch_fn, max_batch_size=100,
                 fail_threshold=5, recovery_timeout=30):
        self.name = name
        self.batch_fn = batch_fn
        self.max_batch_size = max_batch_size
        self.fail_threshold = fail_threshold
        self.recovery_timeout = recovery_timeout
        self.queue = []
        self.failures = 0
        self.state = 'CLOSED'
        self.last_failure = 0

    def load(self, key, fallback=None):
        if self.state == 'OPEN':
            if time.time() - self.last_failure > self.recovery_timeout:
                self.state = 'HALF_OPEN'
            else:
                return fallback
        self.queue.append(key)
        return None

    def execute_batch(self):
        if not self.queue:
            return {}

        batch = self.queue[:self.max_batch_size]
        self.queue = self.queue[self.max_batch_size:]

        try:
            results = self.batch_fn(batch)
            self.failures = 0
            if self.state == 'HALF_OPEN':
                self.state = 'CLOSED'
            return dict(zip(batch, results))
        except Exception:
            self.failures += 1
            self.last_failure = time.time()
            if self.failures >= self.fail_threshold:
                self.state = 'OPEN'
            return {}

def batch_load_users(user_ids):
    time.sleep(0.1)
    return [{"id": uid, "name": f"User {uid}"} for uid in user_ids]

loader = DataLoaderCircuitBreaker("user-loader", batch_load_users)

for uid in range(5):
    result = loader.load(uid, fallback={"name": "Unknown"})
    if result:
        print(f"User {uid}: {result}")

results = loader.execute_batch()
for uid, data in results.items():
    print(f"User {uid}: {data}")

Expected output:

User 0: {'id': 0, 'name': 'User 0'}
User 1: {'id': 1, 'name': 'User 1'}
User 2: {'id': 2, 'name': 'User 2'}

Common Mistakes

  • Circuit breaker at query level instead of resolver level -- a query-level circuit breaker fails the entire query. Use resolver-level breakers for granular partial results. Only the failing field returns null.
  • No null handling in GraphQL schema -- GraphQL schemas must define nullable fields for circuit breaker fallbacks. Required fields cannot return null. Make fields nullable or provide default values.
  • DataLoader without circuit breaker -- DataLoader batches requests efficiently but without circuit breaker protection, a batch call can fail all keys at once. Wrap the batch function with a circuit breaker.
  • Fallback value mismatch with GraphQL type -- fallback values must match the GraphQL schema type. Returning a string when an Int is expected causes GraphQL validation errors. Define type-appropriate fallbacks.
  • Not using @skip or @include directives -- allow clients to skip circuit-protected fields they don't need. GraphQL @skip and @include directives let clients control which fields to query.

Practice Questions

  1. Why should circuit breakers be at the resolver level in GraphQL?
  2. How do partial query results differ from full query failure?
  3. How does DataLoader interact with circuit breakers?
  4. What GraphQL schema considerations are needed for circuit breakers?
  5. How do you provide fallback values that match GraphQL types?

Challenge

Build a GraphQL API with circuit breaker resilience: (1) per-resolver circuit breakers for 5 data sources (products, reviews, inventory, pricing, shipping), (2) each resolver returns fallback values when its circuit is open (null, empty array, cached data), (3) DataLoader with circuit breaker for batch user loading, (4) GraphQL schema with nullable fields for all circuit-protected resolvers, (5) batch loader with circuit breaker that fails individual keys instead of the entire batch, (6) client-side @skip/@include directives for circuit-protected fields, (7) Apollo Federation integration: circuit breakers for federated subgraph calls, (8) metrics: resolver success rate, circuit state per resolver, fallback rate.

FAQ

How does circuit breaker work in GraphQL?

Each resolver has its own circuit breaker for its downstream data source. When the circuit opens for a resolver, it returns a fallback value (null, empty array, cached data) instead of calling the downstream service. Other resolvers continue normally.

What happens when a resolver circuit opens?

The resolver returns its configured fallback value. The GraphQL response includes partial data: successful fields have real data, the failed field has the fallback. Clients must handle nullable fields gracefully.

How does DataLoader benefit from circuit breakers?

DataLoader batches multiple keys into one call. Without a circuit breaker, a single batch failure fails all keys. With a circuit breaker, the batch call is protected: if the circuit is open, each key returns its individual fallback instead of failing together.

Should I use circuit breakers in Apollo Federation?

Yes. Each subgraph should have its own circuit breakers for its data sources. The federation gateway can also have circuit breakers for subgraph calls, providing a second layer of protection at the gateway level.

How do I handle mutations in GraphQL circuit breakers?

Mutations should rarely have fallbacks. If a mutation circuit is open, return an error in the mutation response (errors array) rather than silently succeeding with a fallback. The client must know the mutation failed.

Mini Project

Build a resilient GraphQL gateway: (1) per-resolver circuit breakers for 10 data sources with different thresholds, (2) resolver-level fallbacks: null for nullable fields, empty arrays for list fields, cached values for critical fields, (3) DataLoader circuit breaker for N+1 query protection, (4) batch circuit breaker that falls back individual keys instead of entire batches, (5) Apollo Federation integration: circuit breakers for subgraph HTTP calls, (6) response middleware that adds circuit breaker metadata to response extensions, (7) metrics: resolver-level success/failure/fallback counts, circuit state per resolver, DataLoader batch failure rate, (8) GraphQL playground with circuit breaker state visualization.

What's Next

Continue with Serverless Patterns to learn Serverless circuit breaker patterns. Then explore Edge Computing for edge circuit breaker patterns.

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro