Sebastien LatoMost apps call APIs like this: try await api.fetchUser() Enter fullscreen mode ...
Most apps call APIs like this:
try await api.fetchUser()
That worksβ¦
until the server starts failing.
Then your app does this:
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:
When a system is failing, stop making it worse.
Instead of retrying endlessly, the system must detect failures and pause requests.
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
enum CircuitState {
case closed
case open(until: Date)
case halfOpen
}
Meaning:
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.
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.
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.
Mobile networks are unstable.
Common failure scenarios:
Without circuit breakers:
Circuit breakers prevent this.
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.
Circuit breakers work with retries.
Example flow:
Request
β Failure
β Retry (exponential backoff)
β Failure threshold reached
β Circuit opens
β Requests paused
This prevents retry storms.
Test scenarios:
Verify that:
Avoid:
These cause:
Think:
Request
β Failure Detection
β Circuit Breaker
β Pause Requests
β Recovery Probe
β Resume Traffic
Not:
βJust retry again.β
Circuit breakers give your app:
This is the difference between: