Problem
In the post about AsyncMap we saw how to process collections in a sequential asynchronous manner. However, when working with external APIs that implement rate limiting, we face a critical problem:
// Process 1000 URLs sequentially
let results = try await urls.asyncMap { url in
try await apiClient.fetch(url) // ⚠️ 1000 calls without pause
}
// Error 429: Too Many Requests
Making consecutive calls without pauses can:
- Exceed rate limits: APIs reject with 429 Too Many Requests.
- Saturate external services: overload of simultaneous connections.
- Waste resources: forcing retries consumes more time and bandwidth.
- Temporary blocks: some APIs block the IP after multiple violations.
We need a way to control the pace of sequential operations, adding intentional pauses between each processing.
Solution
We update asyncMap by adding an optional timeout parameter that introduces a configurable pause after processing each element.
extension Sequence {
func asyncMap<T>(
timeout: Double? = nil,
_ transform: (Element) async throws -> T
) async throws -> [T] {
var results = [T]()
results.reserveCapacity(underestimatedCount)
for element in self {
try await results.append(transform(element))
if let timeout {
try await Task.sleep(for: .seconds(timeout))
}
}
return results
}
}
Key changes from the original version:
✨ Optional and backward-compatible timeout: Double? = nil parameter.
⏱️ If timeout is specified, adds Task.sleep(for: .seconds(timeout)) after each element.
🔄 Maintains original behavior when timeout is not specified (no pauses).
📊 Allows dynamic rate limiting adjustment according to each API’s limits.
Result
// Rate limiting: 1 call per second
let results = try await urls.asyncMap(timeout: 1.0) {
try await apiClient.fetch($0)
}
// Aggressive rate limiting: 1 call every 5 seconds
let results = try await endpoints.asyncMap(timeout: 5.0) {
try await scraper.parse($0)
}
// Without timeout: original behavior (maximum speed)
let results = try await localFiles.asyncMap {
try await processFile($0)
}
Benefits of this update:
⏱️ Configurable rate limiting: controls the call pace according to each API’s limits.
🛡️ Block prevention: avoids 429 errors and temporary IP suspensions.
🔄 Full backward compatibility: without timeout it works exactly as before.
🎯 Flexibility per use case: adjusts timeout based on external service tolerance.
📊 Predictable processing: easily calculate total time (n elements × timeout).
This update turns asyncMap into a complete tool for controlled sequential processing, ideal for integration with APIs that impose rate limits and need a regulated request flow.
Keep coding, keep running 🏃♂️