Most apps call APIs like this:
try await api.fetchUser()
That worksβ¦
until the server starts failing.
Then your app does this:
- retries immediately
- sends hundreds of failing requests
- drains battery
- overloads the backend
- slows down the UI
- worsens the outage
This is called a cascading failure.
A production app needs protection mechanisms.
This post shows how to implement circuit breakers and network resilience patterns in SwiftUI that are:
- failure-aware
- backend-friendly
- battery-conscious
- retry-safe
- production-grade
π§ 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 recovery
Flow:
Requests Fail
β Circuit Opens
β Requests Blocked
β Recovery Test
β Circuit Closes
π 2. Define Circuit States
enum CircuitState {
case closed
case open(until: Date)
case halfOpen
}
Meaning:
- 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 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 circuit.
If they fail β reopen.
π 6. Why Mobile Apps Need Circuit Breakers
Mobile networks are unstable.
Common failure scenarios:
- server outage
- DNS issues
- TLS failures
- rate limiting
- cellular packet loss
Without circuit breakers:
- your app spams the backend
- battery drains rapidly
- retries amplify the outage
Circuit breakers prevent this.
π§± 7. Integrating with API Clients
Wrap network calls:
func performRequest<T>(_ 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 paused
This 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
- battery drain
π§ Mental Model
Think:
Request
β Failure Detection
β Circuit Breaker
β Pause Requests
β Recovery Probe
β Resume Traffic
Not:
βJust retry again.β
π Final Thoughts
Circuit breakers give your app:
- backend protection
- smarter retry behavior
- reduced battery usage
- resilience during outages
- predictable recovery
This is the difference between:
- a fragile network client
- and a production-grade mobile system
Top comments (0)