Most apps call APIs like this:
try await api.fetchFeed()
That worksβ¦
until something triggers many requests at once.
Examples:
- infinite refresh loops
- aggressive background sync
- multiple views requesting the same data
- retry storms after network recovery
- multiple devices syncing simultaneously
Suddenly your app becomes the biggest threat to your own backend.
This post shows how to implement rate limiting and backpressure in SwiftUI so your networking layer is:
- backend-safe
- quota-aware
- retry-friendly
- battery-conscious
- production-grade
π§ The Core Principle
A healthy system controls its request rate.
Even when everything is working, uncontrolled traffic can overload APIs.
π§± 1. What Is Rate Limiting?
Rate limiting controls how many requests can occur during a time window.
Example rule:
10 requests per second
If more requests arrive, they must:
- wait
- queue
- or be rejected
This protects both:
- the backend
- the client device
𧬠2. Token Bucket Model
A common approach is the token bucket algorithm.
Concept:
Bucket contains tokens
Each request consumes one token
Tokens refill over time
If the bucket is empty:
Request must wait
π§± 3. Simple Rate Limiter Implementation
actor RateLimiter {
private let maxTokens: Int
private let refillInterval: TimeInterval
private var tokens: Int
private var lastRefill: Date
init(maxTokens: Int, refillInterval: TimeInterval) {
self.maxTokens = maxTokens
self.refillInterval = refillInterval
self.tokens = maxTokens
self.lastRefill = Date()
}
func acquire() async {
refill()
while tokens == 0 {
try? await Task.sleep(nanoseconds: 100_000_000)
refill()
}
tokens -= 1
}
private func refill() {
let now = Date()
let elapsed = now.timeIntervalSince(lastRefill)
if elapsed >= refillInterval {
tokens = maxTokens
lastRefill = now
}
}
}
This ensures requests happen at a controlled pace.
π¦ 4. Using the Rate Limiter
Wrap your API calls:
let limiter = RateLimiter(maxTokens: 5, refillInterval: 1)
func fetchFeed() async throws -> Feed {
await limiter.acquire()
return try await api.fetchFeed()
}
Now your app cannot exceed 5 requests per second.
π 5. What Is Backpressure?
Backpressure prevents systems from producing more work than they can handle.
Example scenario:
Scroll view triggers 50 image loads
Without backpressure:
50 network requests instantly
With backpressure:
Requests queue and execute gradually
This protects:
- CPU
- memory
- network
- backend APIs
π¦ 6. Request Queue Pattern
Queue requests instead of executing immediately.
actor RequestQueue {
private var queue: [() async -> Void] = []
func enqueue(_ task: @escaping () async -> Void) {
queue.append(task)
processNext()
}
private func processNext() {
guard !queue.isEmpty else { return }
let task = queue.removeFirst()
Task {
await task()
processNext()
}
}
}
This ensures controlled execution.
π 7. Why Mobile Apps Need Rate Limiting
Mobile environments are unpredictable.
Common issues:
- API quotas
- slow cellular connections
- background sync bursts
- multi-tab refresh loops
Without limits:
- the backend gets flooded
- battery drains faster
- UI becomes unstable
Rate limiting stabilizes the system.
π 8. Combine With Circuit Breakers
From the previous post:
Circuit Breakers β stop requests during failures
Rate Limiting β control requests during success
Together they form complete network resilience.
π§ͺ 9. Testing Rate Limiting
Test scenarios:
- rapid refresh loops
- large scroll lists triggering network calls
- background sync bursts
- retry storms
Verify that:
- request rate remains stable
- the queue processes correctly
- no backend overload occurs
β οΈ 10. Common Anti-Patterns
Avoid:
- unlimited parallel requests
- retrying immediately after failure
- per-view network calls without coordination
- ignoring server rate limits
- not deduplicating identical requests
These cause:
- request storms
- API bans
- battery drain
- backend instability
π§ Mental Model
Think:
User Action
β Request Queue
β Rate Limiter
β Network Request
β API
Not:
βFire every request immediately.β
π Final Thoughts
Rate limiting and backpressure give your app:
- predictable network behavior
- backend protection
- smoother performance
- better battery usage
- production-grade resilience
This is the difference between:
- an app that overwhelms its backend
- and a system that scales safely
Top comments (0)