SwiftUI Circuit Breakers & Network Resilience Patterns (Prevent Cascading Failures)
Source: Dev.to
Most apps call APIs like this:
try await api.fetchUser()That works… until the server starts failing. Then your app may:
- retry immediately
- send hundreds of failing requests
- drain battery
- overload the backend
- slow down the UI
- worsen the outage
This is called a cascading failure. A production app needs protection mechanisms.
🧠 The Core Principle
When a system is failing, stop making it worse. Instead of retrying endlessly, the system must detect failures and pause requests.
🧱 1. What Is a Circuit Breaker?
A circuit breaker prevents repeated calls to a failing service. It has three states:
Closed → Normal operation
Open → Requests blocked
Half-Open → Testing recoveryFlow
Requests Fail
→ Circuit Opens
→ Requests Blocked
→ Recovery Test
→ Circuit Closes🔌 2. Define Circuit States
enum CircuitState {
case closed
case open(until: Date)
case halfOpen
}- Closed → requests allowed
- Open → requests blocked temporarily
- Half‑Open → allow limited requests to test recovery
🧬 3. Basic Circuit Breaker Implementation
final class CircuitBreaker {
private(set) var state: CircuitState = .closed
private var failureCount = 0
private let failureThreshold = 5
private let timeout: TimeInterval = 30
func recordFailure() {
failureCount += 1
if failureCount >= failureThreshold {
state = .open(until: Date().addingTimeInterval(timeout))
}
}
func recordSuccess() {
failureCount = 0
state = .closed
}
}Failures accumulate until the circuit opens.
🚦 4. Blocking Requests When Circuit Is Open
Before performing a request:
func canExecute() -> Bool {
switch state {
case .closed:
return true
case .open(let until):
return Date() > until
case .halfOpen:
return true
}
}Usage
guard breaker.canExecute() else {
throw NetworkError.circuitOpen
}This protects the backend.
🔄 5. Transition to Half‑Open
When the timeout expires:
case .open(let until):
if Date() > until {
state = .halfOpen
return true
}Only a small number of requests should test recovery. If they succeed → close the circuit; if they fail → reopen.
🔋 6. Why Mobile Apps Need Circuit Breakers
Mobile networks are unstable. Common failure scenarios include:
- server outage
- DNS issues
- TLS failures
- rate limiting
- cellular packet loss
Without circuit breakers, your app may:
- spam the backend
- drain battery rapidly
- amplify the outage
Circuit breakers prevent this.
🧱 7. Integrating with API Clients
Wrap network calls:
func performRequest(_ operation: () async throws -> T) async throws -> T {
guard breaker.canExecute() else {
throw NetworkError.circuitOpen
}
do {
let result = try await operation()
breaker.recordSuccess()
return result
} catch {
breaker.recordFailure()
throw error
}
}This makes resilience automatic.
🌐 8. Combine with Retry Strategies
Circuit breakers work with retries. Example flow:
Request
→ Failure
→ Retry (exponential backoff)
→ Failure threshold reached
→ Circuit opens
→ Requests pausedThis prevents retry storms.
🧪 9. Testing Circuit Breakers
Test scenarios:
- repeated server errors
- timeout storms
- network disconnections
- recovery after outage
- half‑open transition
Verify that:
- requests stop during outages
- requests resume after recovery
⚠️ 10. Common Anti‑Patterns
Avoid:
- infinite retries
- retrying immediately after failure
- ignoring server rate limits
- retrying while offline
- not tracking failure counts
These cause backend overload, cascading outages, and battery drain.
🧠 Mental Model
Request
→ Failure Detection
→ Circuit Breaker
→ Pause Requests
→ Recovery Probe
→ Resume TrafficNot: “Just retry again.”
🚀 Final Thoughts
Circuit breakers give your app:
- backend protection
- smarter retry behavior
- reduced battery usage
- resilience during outages
- predictable recovery
They are the difference between a fragile network client and a production‑grade mobile system.