DEV Community

Sebastien Lato
Sebastien Lato

Posted on

SwiftUI Rate Limiting & Backpressure (Protect Your Backend From Your Own App)

Most apps call APIs like this:

try await api.fetchFeed()
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

If the bucket is empty:

Request must wait
Enter fullscreen mode Exit fullscreen mode

🧱 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
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

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()
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Without backpressure:

50 network requests instantly
Enter fullscreen mode Exit fullscreen mode

With backpressure:

Requests queue and execute gradually
Enter fullscreen mode Exit fullscreen mode

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()
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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)