Core Circuit Breaker
The Circuit Breaker pattern protects your application from cascading failures by automatically stopping requests to failing services. When a service is experiencing issues, the circuit breaker “opens” and immediately rejects requests, preventing resource exhaustion and allowing the service time to recover.
Overview
Section titled “Overview”Circuit breakers operate in three states:
- CLOSED: Normal operation, allowing all requests through while monitoring for failures
- OPEN: Circuit is open, immediately rejecting requests without attempting execution
- HALF_OPEN: Testing if service has recovered, allowing limited requests to probe recovery
State transitions occur automatically based on failure thresholds and timeouts, helping preserve system resources during backend outages.
CircuitBreakerOptions Configuration
Section titled “CircuitBreakerOptions Configuration”CircuitBreakerOptions controls the thresholds and timeouts that determine when the circuit breaker transitions between states, and which exceptions should be ignored.
| Property | Type | Default | Description |
|---|---|---|---|
failureThreshold | int | 5 | Number of consecutive failures required to open the circuit. When the failure count reaches this threshold, the circuit transitions from Closed to Open state. |
resetTimeout | Duration | 60 seconds | Duration to wait before attempting to reset the circuit (transition to Half-Open). After the circuit opens, it waits for this duration before allowing a test request to check if the service has recovered. |
successThreshold | int | 1 | Number of consecutive successes required in Half-Open state to close the circuit. When the success count reaches this threshold while in Half-Open state, the circuit transitions back to Closed state. |
ignoreExceptions | List<Type> | [] | List of exception types that should not trip the circuit breaker. When an exception of one of these types (or a subtype) is thrown, it will not be counted as a failure. Useful for client errors like 404 (not found) that shouldn’t cause the circuit to open. |
Configuration Example
Section titled “Configuration Example”final options = CircuitBreakerOptions( failureThreshold: 5, // Open after 5 consecutive failures resetTimeout: Duration(seconds: 60), // Wait 60s before testing recovery successThreshold: 1, // Close after 1 success in half-open ignoreExceptions: [FormatException], // Don't count format errors as failures);Integration with QueryClient
Section titled “Integration with QueryClient”Enable circuit breaker protection globally by providing a CircuitBreakerRegistry to the QueryClient constructor. All queries created through this client will automatically use circuit breaker protection.
final registry = CircuitBreakerRegistry();final client = QueryClient( circuitBreakerRegistry: registry,);
final query = client.getQuery<User>( 'user'.toQueryKey(), () => api.fetchUser(),);[!IMPORTANT] If no
CircuitBreakerRegistryis provided toQueryClient, circuit breaker functionality is disabled for all queries. Individual queries can still enable circuit breakers by providingcircuitBreakeroptions inQueryOptions.
Per-Query Configuration
Section titled “Per-Query Configuration”Configure circuit breaker behavior for individual queries using QueryOptions. This allows fine-grained control over which queries are protected and how they behave.
Basic Per-Query Configuration
Section titled “Basic Per-Query Configuration”QueryBuilder<User>( queryKey: 'user'.toQueryKey(), queryFn: () => api.fetchUser(), options: QueryOptions( circuitBreaker: CircuitBreakerOptions( failureThreshold: 3, // More sensitive for this query resetTimeout: Duration(seconds: 30), // Faster recovery ), ), builder: (context, state) { // ... },)Custom Scope for Multiple Queries
Section titled “Custom Scope for Multiple Queries”Use circuitBreakerScope to group multiple queries under a single circuit breaker. This is useful when multiple queries hit the same service or endpoint.
// All queries with this scope share the same circuit breakerQueryBuilder<List<User>>( queryKey: 'users'.toQueryKey(), queryFn: () => api.fetchUsers(), options: QueryOptions( circuitBreakerScope: 'api.example.com', circuitBreaker: CircuitBreakerOptions( failureThreshold: 5, ), ), builder: (context, state) { // ... },)
// This query also uses the same circuit breakerQueryBuilder<User>( queryKey: 'user-123'.toQueryKey(), queryFn: () => api.fetchUser(123), options: QueryOptions( circuitBreakerScope: 'api.example.com', // Same scope = same circuit ), builder: (context, state) { // ... },)[!NOTE] By default, circuit breakers are scoped to the query key. Use
circuitBreakerScopeto override this behavior and group queries by service, domain, or any other logical grouping.
CircuitBreakerOpenException
Section titled “CircuitBreakerOpenException”When a circuit breaker is in the OPEN state, requests are immediately rejected and a CircuitBreakerOpenException is thrown. This exception provides information about which circuit is open and why the request was rejected.
Exception Properties
Section titled “Exception Properties”| Property | Type | Description |
|---|---|---|
message | String | Descriptive message explaining why the exception was thrown |
circuitScope | String? | Optional scope or identifier of the circuit that is currently open. Useful for logging and debugging. |
Handling CircuitBreakerOpenException
Section titled “Handling CircuitBreakerOpenException”QueryBuilder<User>( queryKey: 'user'.toQueryKey(), queryFn: () => api.fetchUser(), builder: (context, state) { if (state.hasError) { if (state.error is CircuitBreakerOpenException) { final exception = state.error as CircuitBreakerOpenException; return Column( children: [ Text('Service temporarily unavailable'), Text('Circuit: ${exception.circuitScope ?? "unknown"}'), ElevatedButton( onPressed: () => QueryClient().invalidateQuery('user'.toQueryKey()), child: Text('Retry'), ), ], ); } return Text('Error: ${state.error}'); }
if (state.isLoading) return CircularProgressIndicator(); return Text('User: ${state.data?.name}'); },)Graceful Fallback
Section titled “Graceful Fallback”Future<User> fetchUserWithFallback() async { try { final query = QueryClient().getQuery<User>( 'user'.toQueryKey(), () => api.fetchUser(), ); return await query.fetch(); } on CircuitBreakerOpenException catch (e) { // Circuit is open, use cached data or default final cached = QueryClient().getQueryData<User>('user'.toQueryKey()); if (cached != null) { return cached; } // Return default or throw throw Exception('Service unavailable and no cached data'); }}Circuit State Machine
Section titled “Circuit State Machine”The circuit breaker automatically transitions between states based on failure counts and timeouts:
CLOSED → OPEN → HALF_OPEN → CLOSED ↑ ↓ └──────────────────────────────┘State Transitions
Section titled “State Transitions”-
CLOSED → OPEN: When
failureThresholdconsecutive failures occur, the circuit opens and immediately rejects all requests. -
OPEN → HALF_OPEN: After
resetTimeoutduration has elapsed, the circuit transitions to half-open state, allowing a test request to check if the service has recovered. -
HALF_OPEN → CLOSED: If
successThresholdconsecutive successes occur in half-open state, the circuit closes and normal operation resumes. -
HALF_OPEN → OPEN: If any failure occurs in half-open state, the circuit immediately opens again and the reset timeout restarts.
[!IMPORTANT] State transitions are automatic and based on the configured thresholds. You cannot manually control circuit states, but you can reset circuits through the
CircuitBreakerRegistryif needed.
Code Examples
Section titled “Code Examples”Global Configuration
Section titled “Global Configuration”Configure circuit breaker protection for all queries in your application:
void main() { final registry = CircuitBreakerRegistry(); final client = QueryClient( circuitBreakerRegistry: registry, );
runApp( QueryClientProvider( client: client, child: MyApp(), ), );}Per-Query Configuration
Section titled “Per-Query Configuration”Configure circuit breaker behavior for a specific query:
QueryBuilder<List<Product>>( queryKey: 'products'.toQueryKey(), queryFn: () => api.fetchProducts(), options: QueryOptions( circuitBreaker: CircuitBreakerOptions( failureThreshold: 3, resetTimeout: Duration(seconds: 30), ignoreExceptions: [FormatException], ), ), builder: (context, state) { if (state.hasError) { if (state.error is CircuitBreakerOpenException) { return Text('Service temporarily unavailable'); } return Text('Error: ${state.error}'); }
if (state.isLoading) return CircularProgressIndicator(); return ListView.builder( itemCount: state.data?.length ?? 0, itemBuilder: (context, index) => ProductTile(state.data![index]), ); },)Shared Circuit via Scope
Section titled “Shared Circuit via Scope”Group multiple queries under a single circuit breaker:
// All queries to the same API share one circuit breakerfinal apiScope = 'api.example.com';
QueryBuilder<List<User>>( queryKey: 'users'.toQueryKey(), queryFn: () => api.fetchUsers(), options: QueryOptions( circuitBreakerScope: apiScope, circuitBreaker: CircuitBreakerOptions(failureThreshold: 5), ), builder: (context, state) => UserList(state.data),)
QueryBuilder<User>( queryKey: 'user-123'.toQueryKey(), queryFn: () => api.fetchUser(123), options: QueryOptions( circuitBreakerScope: apiScope, // Same circuit as above ), builder: (context, state) => UserDetail(state.data),)Manual Registry Management
Section titled “Manual Registry Management”For advanced use cases, you can manage circuit breakers manually:
final registry = CircuitBreakerRegistry();
// Register callback for circuit open eventsregistry.registerCircuitOpenCallback((event) { print('Circuit ${event.circuitId} opened at ${event.openedAt}'); // Send to monitoring service, show user notification, etc.});
// Get or create a circuit breakerfinal options = CircuitBreakerOptions(failureThreshold: 5);final breaker = registry.getOrCreate('api.example.com', options);
// Check circuit stateif (breaker.allowRequest()) { // Proceed with request} else { // Circuit is open, handle accordingly}
// Reset a circuit manually (useful for testing or manual recovery)registry.reset('api.example.com');Best Practices
Section titled “Best Practices”Defending Against Third-Party API Failures
Section titled “Defending Against Third-Party API Failures”When integrating with external services, use circuit breakers to prevent hammering a failing service:
QueryBuilder<WeatherData>( queryKey: 'weather'.toQueryKey(), queryFn: () => weatherApi.getCurrentWeather(), options: QueryOptions( circuitBreakerScope: 'weather-api.example.com', circuitBreaker: CircuitBreakerOptions( failureThreshold: 3, // Fail fast for external services resetTimeout: Duration(minutes: 2), // Give service time to recover successThreshold: 2, // Require 2 successes to close ), ), builder: (context, state) { if (state.hasError && state.error is CircuitBreakerOpenException) { // Show cached data or a "service unavailable" message return CachedWeatherDisplay(); } return WeatherDisplay(state.data); },)Ignoring Client Errors
Section titled “Ignoring Client Errors”Some errors (like 404 Not Found or 401 Unauthorized) are client errors and shouldn’t trip the circuit breaker:
QueryBuilder<User>( queryKey: 'user'.toQueryKey(), queryFn: () => api.fetchUser(), options: QueryOptions( circuitBreaker: CircuitBreakerOptions( failureThreshold: 5, ignoreExceptions: [ FormatException, // Invalid request format ArgumentError, // Client-side errors // Add custom exception types as needed ], ), ), builder: (context, state) { // ... },)[!NOTE] The
ignoreExceptionslist uses type matching. If you addExceptionto the list, all exceptions will be ignored. Be specific about which exception types should not trip the circuit.
Shared Circuits via Scope
Section titled “Shared Circuits via Scope”Group queries by service or domain to share circuit breaker state:
// All queries to the payment service share one circuitfinal paymentScope = 'payment-api.example.com';
QueryBuilder<PaymentMethod>( queryKey: 'payment-methods'.toQueryKey(), queryFn: () => paymentApi.getPaymentMethods(), options: QueryOptions( circuitBreakerScope: paymentScope, circuitBreaker: CircuitBreakerOptions(failureThreshold: 3), ), builder: (context, state) => PaymentMethodList(state.data),)
QueryBuilder<PaymentResult>( queryKey: 'process-payment'.toQueryKey(), queryFn: () => paymentApi.processPayment(), options: QueryOptions( circuitBreakerScope: paymentScope, // Same circuit ), builder: (context, state) => PaymentResult(state.data),)Monitoring Circuit States
Section titled “Monitoring Circuit States”Register callbacks to monitor when circuits open:
final registry = CircuitBreakerRegistry();
registry.registerCircuitOpenCallback((event) { // Log to monitoring service analytics.track('circuit_opened', { 'circuit_id': event.circuitId, 'opened_at': event.openedAt.toIso8601String(), });
// Show user notification showNotification('Service temporarily unavailable');
// Alert operations team alertingService.sendAlert( 'Circuit breaker opened for ${event.circuitId}', );});Troubleshooting
Section titled “Troubleshooting”Circuit Opens Too Frequently
Section titled “Circuit Opens Too Frequently”If your circuit breaker opens too often, consider:
- Increase
failureThreshold: Allow more failures before opening - Review
ignoreExceptions: Add exception types that shouldn’t count as failures - Check for transient errors: Some errors might be temporary and shouldn’t trip the circuit
CircuitBreakerOptions( failureThreshold: 10, // More lenient ignoreExceptions: [TimeoutException], // Don't count timeouts)Circuit Never Recovers
Section titled “Circuit Never Recovers”If the circuit stays open, check:
resetTimeouttoo long: Reduce the timeout to test recovery sooner- Service still failing: The service might still be down; check service health
- Success threshold too high: Reduce
successThresholdto close faster
CircuitBreakerOptions( resetTimeout: Duration(seconds: 30), // Test recovery sooner successThreshold: 1, // Close after first success)Circuit Opens for Expected Errors
Section titled “Circuit Opens for Expected Errors”If the circuit opens for errors that shouldn’t count as failures:
- Add to
ignoreExceptions: Include exception types that are expected - Review error handling: Ensure client errors (4xx) are handled separately
CircuitBreakerOptions( ignoreExceptions: [ FormatException, ArgumentError, // Add your custom exception types ],)Multiple Queries Not Sharing Circuit
Section titled “Multiple Queries Not Sharing Circuit”If queries that should share a circuit don’t:
- Check
circuitBreakerScope: Ensure all queries use the same scope value - Verify registry: Make sure all queries use the same
QueryClientwith the same registry
// All must use the same scope stringoptions: QueryOptions( circuitBreakerScope: 'api.example.com', // Must match exactly)