Rate limiting и throttling в Swift: защита и оптимизация API

Онлайн-тренажер Python для начинающих

Изучайте Python легко и без перегрузки теорией. Решайте практические задачи с автоматической проверкой, получайте подсказки на русском языке и пишите код прямо в браузере — без необходимости что-либо устанавливать.

Начать курс

Ваш API под ударом: почему rate limiting и throttling — must have для Swift-разработчика

Представьте: вы запустили крутое приложение на Swift, пользователи в восторге, серверы работают идеально. Но внезапно — коллапс. Кто-то запустил скрипт, который долбит ваш API тысячами запросов в секунду. Или просто один активный пользователь спамит кнопкой «Обновить» быстрее, чем вы успеваете обработать данные. Результат — тормоза, падение сервера и негативные отзывы.

Знакомо? Тогда эта статья для вас. Мы разберем rate limiting и throttling в Swift — техники, которые спасут ваш бэкенд и сделают приложение стабильным. Вы узнаете, как реализовать ограничение запросов в Swift на стороне клиента, используя современные подходы с async/await, и какие паттерны действительно работают.

Rate limiting vs Throttling: в чем разница?

Прежде чем писать код, давайте разберемся с терминами. Многие путают эти понятия, хотя они решают разные задачи.

Rate limiting (ограничение скорости)

Это жесткий лимит: «Не больше N запросов за период». Если лимит превышен — запрос отклоняется. Сервер обычно возвращает 429 Too Many Requests. Пример: вы разрешаете не более 100 запросов в минуту с одного IP.

Throttling (дросселирование)

Это более мягкий механизм: запросы не блокируются, а ставятся в очередь и выполняются с заданной частотой. Представьте, что ваш API может обрабатывать только 5 запросов в секунду. Throttling выстроит их в очередь и отправит с нужной скоростью, не перегружая сервер.

В Swift оба подхода реализуются по-разному. Давайте рассмотрим лучшие практики.

Практические реализации rate limiting в Swift

1. Простой таймерный throttling (GCD)

Самый простой способ ограничить запросы в Swift — использовать DispatchQueue и таймер. Вот пример, который гарантирует, что запросы будут отправляться не чаще одного раза в секунду:

import Foundation

final class RequestThrottler {
    private let queue = DispatchQueue(label: "com.app.throttle")
    private var lastRequestTime: Date = .distantPast
    private let minimumInterval: TimeInterval
    
    init(interval: TimeInterval = 1.0) {
        self.minimumInterval = interval
    }
    
    func performThrottledRequest(block: @escaping () -> Void) {
        queue.sync {
            let now = Date()
            if now.timeIntervalSince(lastRequestTime) >= minimumInterval {
                lastRequestTime = now
                DispatchQueue.main.async {
                    block()
                }
            } else {
                print("Запрос отклонен: слишком часто")
            }
        }
    }
}

// Использование
let throttler = RequestThrottler(interval: 2.0)
throttler.performThrottledRequest {
    // Ваш API-запрос
    print("Запрос выполнен")
}

Этот подход прост, но не идеален: он просто отбрасывает «лишние» запросы. Для многих сценариев этого достаточно, но если вам нужно сохранить порядок и гарантировать выполнение — читайте дальше.

2. Rate limiter с очередью (async/await)

С появлением Swift async/await мы получили элегантный способ реализации swift rate limiter с поддержкой очереди. Вот как это выглядит:

import Foundation

actor RateLimiter {
    private let maxRequests: Int
    private let perSecond: Double
    private var requestTimestamps: [Date] = []
    private var pendingTasks: [CheckedContinuation] = []
    private var isProcessing = false
    
    init(maxRequests: Int, perSecond: Double) {
        self.maxRequests = maxRequests
        self.perSecond = perSecond
    }
    
    func waitForSlot() async throws {
        try await withCheckedThrowingContinuation { continuation in
            pendingTasks.append(continuation)
            if !isProcessing {
                processQueue()
            }
        }
    }
    
    private func processQueue() {
        isProcessing = true
        Task {
            while !pendingTasks.isEmpty {
                let now = Date()
                // Удаляем старые записи (старше 1 секунды)
                requestTimestamps = requestTimestamps.filter { 
                    now.timeIntervalSince($0) < 1.0 
                }
                
                if requestTimestamps.count < maxRequests {
                    // Можно выполнять запрос
                    let continuation = pendingTasks.removeFirst()
                    requestTimestamps.append(now)
                    continuation.resume()
                } else {
                    // Нужно подождать
                    let sleepTime = 1.0 / perSecond
                    try? await Task.sleep(nanoseconds: UInt64(sleepTime * 1_000_000_000))
                }
            }
            isProcessing = false
        }
    }
}

// Использование
let limiter = RateLimiter(maxRequests: 5, perSecond: 1.0)

func makeAPICall() async throws {
    try await limiter.waitForSlot()
    // Ваш API-запрос
    let (data, _) = try await URLSession.shared.data(from: URL(string: "https://api.example.com")!)
    print("Получено \(data.count) байт")
}

// Тест: 10 запросов подряд
Task {
    for i in 0..<10 {
        Task {
            try? await makeAPICall()
            print("Запрос \(i) завершен")
        }
    }
}

Этот swift rate limiter использует actor для потокобезопасности и очередь с ожиданием. Он не отбрасывает запросы, а ставит их в буфер и выполняет с заданной скоростью. Идеально для работы с API, где важно не потерять ни один запрос.

3. Token Bucket алгоритм (продвинутый уровень)

Token bucket в Swift — один из самых популярных алгоритмов rate limiting. Представьте ведро, в которое каждую секунду падает N токенов. Каждый запрос забирает один токен. Если токенов нет — запрос отклоняется или ждет.

import Foundation

final class TokenBucket {
    private let capacity: Int
    private let refillRate: Int // токенов в секунду
    private var tokens: Int
    private var lastRefillTime: Date
    private let queue = DispatchQueue(label: "com.tokenbucket")
    
    init(capacity: Int, refillRate: Int) {
        self.capacity = capacity
        self.refillRate = refillRate
        self.tokens = capacity
        self.lastRefillTime = Date()
    }
    
    func tryConsume() -> Bool {
        queue.sync {
            refill()
            if tokens > 0 {
                tokens -= 1
                return true
            }
            return false
        }
    }
    
    private func refill() {
        let now = Date()
        let elapsed = now.timeIntervalSince(lastRefillTime)
        let tokensToAdd = Int(elapsed) * refillRate
        if tokensToAdd > 0 {
            tokens = min(capacity, tokens + tokensToAdd)
            lastRefillTime = now
        }
    }
}

// Использование
let bucket = TokenBucket(capacity: 10, refillRate: 2) // 10 токенов, пополнение 2/сек

func apiCallWithToken() {
    if bucket.tryConsume() {
        print("Запрос выполнен")
        // Ваш код
    } else {
        print("Лимит превышен, подождите")
        // Можно показать UI или поставить в очередь
    }
}

for _ in 0..<15 {
    apiCallWithToken()
    Thread.sleep(forTimeInterval: 0.1)
}

Token bucket Swift отлично подходит для ситуаций, когда у вас есть пиковые нагрузки, но в среднем трафик стабильный. Он «сглаживает» всплески, позволяя накопить ток

Блоги

Рекомендаций по книгам