Async Map Timeout


My 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.

My 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.

My 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 ๐Ÿƒโ€โ™‚๏ธ