Table of contents

How do I implement request deduplication with Alamofire?

Request deduplication is a crucial optimization technique that prevents redundant network calls by identifying and eliminating duplicate requests. In Alamofire, you can implement this using custom interceptors, request tracking mechanisms, and caching strategies to improve application performance and reduce unnecessary network traffic.

Understanding Request Deduplication

Request deduplication works by creating unique identifiers for requests and tracking ongoing operations. When a new request matches an existing one, instead of making another network call, the system either:

  • Returns the cached response from a previous identical request
  • Waits for the ongoing request to complete and shares its result
  • Cancels the duplicate request entirely

This is particularly useful in scenarios where users might trigger the same API call multiple times quickly, or when implementing features like search-as-you-type functionality.

Basic Request Deduplication Implementation

Here's a fundamental approach using a custom request interceptor:

import Alamofire
import Foundation

class RequestDeduplicationInterceptor: RequestInterceptor {
    private var ongoingRequests: [String: DataRequest] = [:]
    private let queue = DispatchQueue(label: "request.deduplication", attributes: .concurrent)

    func adapt(_ urlRequest: URLRequest, for session: Session, completion: @escaping (Result<URLRequest, Error>) -> Void) {
        completion(.success(urlRequest))
    }

    func retry(_ request: Request, for session: Session, dueTo error: Error, completion: @escaping (RetryResult) -> Void) {
        let requestKey = createRequestKey(from: request.request!)

        queue.async(flags: .barrier) {
            self.ongoingRequests.removeValue(forKey: requestKey)
        }

        completion(.doNotRetry)
    }

    private func createRequestKey(from request: URLRequest) -> String {
        var components: [String] = []

        if let url = request.url?.absoluteString {
            components.append(url)
        }

        components.append(request.httpMethod ?? "GET")

        if let httpBody = request.httpBody,
           let bodyString = String(data: httpBody, encoding: .utf8) {
            components.append(bodyString)
        }

        return components.joined(separator: "|")
    }
}

Advanced Deduplication with Response Sharing

For more sophisticated deduplication that shares responses between duplicate requests:

class AdvancedDeduplicationManager {
    private var pendingRequests: [String: [((Result<Any, AFError>) -> Void)]] = [:]
    private var responseCache: [String: (response: Any, timestamp: Date)] = [:]
    private let cacheTimeout: TimeInterval = 300 // 5 minutes
    private let queue = DispatchQueue(label: "deduplication.manager", attributes: .concurrent)

    func performRequest<T: Codable>(
        _ request: URLRequest,
        responseType: T.Type,
        completion: @escaping (Result<T, AFError>) -> Void
    ) {
        let requestKey = createRequestKey(from: request)

        queue.async(flags: .barrier) {
            // Check if we have a cached response
            if let cachedItem = self.responseCache[requestKey],
               Date().timeIntervalSince(cachedItem.timestamp) < self.cacheTimeout,
               let cachedResponse = cachedItem.response as? T {
                DispatchQueue.main.async {
                    completion(.success(cachedResponse))
                }
                return
            }

            // Check if request is already pending
            if self.pendingRequests[requestKey] != nil {
                // Add callback to existing request
                self.pendingRequests[requestKey]?.append { result in
                    if let response = try? result.get() as? T {
                        completion(.success(response))
                    } else if case .failure(let error) = result as? Result<T, AFError> {
                        completion(.failure(error))
                    }
                }
                return
            }

            // Initialize pending request array
            self.pendingRequests[requestKey] = [{ result in
                if let response = try? result.get() as? T {
                    completion(.success(response))
                } else if case .failure(let error) = result as? Result<T, AFError> {
                    completion(.failure(error))
                }
            }]

            // Make the actual request
            AF.request(request)
                .validate()
                .responseDecodable(of: responseType) { [weak self] response in
                    self?.queue.async(flags: .barrier) {
                        let callbacks = self?.pendingRequests.removeValue(forKey: requestKey) ?? []

                        switch response.result {
                        case .success(let data):
                            // Cache successful response
                            self?.responseCache[requestKey] = (response: data, timestamp: Date())

                            // Notify all waiting callbacks
                            DispatchQueue.main.async {
                                callbacks.forEach { callback in
                                    callback(.success(data as Any))
                                }
                            }

                        case .failure(let error):
                            // Notify all callbacks of failure
                            DispatchQueue.main.async {
                                callbacks.forEach { callback in
                                    callback(.failure(error))
                                }
                            }
                        }
                    }
                }
        }
    }

    private func createRequestKey(from request: URLRequest) -> String {
        var hasher = Hasher()
        hasher.combine(request.url?.absoluteString)
        hasher.combine(request.httpMethod)

        if let httpBody = request.httpBody {
            hasher.combine(httpBody)
        }

        if let headers = request.allHTTPHeaderFields {
            for (key, value) in headers.sorted(by: { $0.key < $1.key }) {
                hasher.combine(key)
                hasher.combine(value)
            }
        }

        return String(hasher.finalize())
    }

    func clearCache() {
        queue.async(flags: .barrier) {
            self.responseCache.removeAll()
        }
    }
}

Implementing Custom Session with Deduplication

Create a custom Alamofire session that automatically handles deduplication:

class DeduplicatingSession {
    private let session: Session
    private let deduplicationManager = AdvancedDeduplicationManager()

    init() {
        let configuration = URLSessionConfiguration.default
        configuration.timeoutIntervalForRequest = 30
        configuration.timeoutIntervalForResource = 60

        self.session = Session(
            configuration: configuration,
            interceptor: RequestDeduplicationInterceptor()
        )
    }

    func request<T: Codable>(
        _ url: String,
        method: HTTPMethod = .get,
        parameters: Parameters? = nil,
        encoding: ParameterEncoding = URLEncoding.default,
        headers: HTTPHeaders? = nil,
        responseType: T.Type,
        completion: @escaping (Result<T, AFError>) -> Void
    ) {
        do {
            let urlRequest = try URLRequest(
                url: url,
                method: method,
                headers: headers
            )

            let encodedRequest = try encoding.encode(urlRequest, with: parameters)

            deduplicationManager.performRequest(
                encodedRequest,
                responseType: responseType,
                completion: completion
            )
        } catch {
            completion(.failure(.parameterEncodingFailed(reason: .jsonEncodingFailed(error: error))))
        }
    }
}

Usage Examples

Here's how to use the deduplicating session in practice:

let deduplicatingSession = DeduplicatingSession()

// Multiple rapid calls to the same endpoint
for i in 1...5 {
    deduplicatingSession.request(
        "https://api.example.com/users/123",
        responseType: User.self
    ) { result in
        switch result {
        case .success(let user):
            print("Request \(i): Received user \(user.name)")
        case .failure(let error):
            print("Request \(i): Failed with error \(error)")
        }
    }
}
// Only one actual network request will be made, but all callbacks will receive the response

Search Implementation with Deduplication

A practical example for search functionality where deduplication prevents excessive API calls:

class SearchManager {
    private let deduplicatingSession = DeduplicatingSession()
    private var searchWorkItem: DispatchWorkItem?

    func search(query: String, completion: @escaping (Result<[SearchResult], AFError>) -> Void) {
        // Cancel previous search if still pending
        searchWorkItem?.cancel()

        // Debounce rapid searches
        searchWorkItem = DispatchWorkItem { [weak self] in
            guard !query.isEmpty else {
                completion(.success([]))
                return
            }

            self?.deduplicatingSession.request(
                "https://api.example.com/search",
                method: .get,
                parameters: ["q": query, "limit": 20],
                responseType: SearchResponse.self
            ) { result in
                switch result {
                case .success(let response):
                    completion(.success(response.results))
                case .failure(let error):
                    completion(.failure(error))
                }
            }
        }

        DispatchQueue.main.asyncAfter(deadline: .now() + 0.3, execute: searchWorkItem!)
    }
}

struct SearchResponse: Codable {
    let results: [SearchResult]
}

struct SearchResult: Codable {
    let id: String
    let title: String
    let description: String
}

Testing Request Deduplication

Here's how to test your deduplication implementation:

import XCTest
@testable import YourApp

class RequestDeduplicationTests: XCTestCase {
    var deduplicationManager: AdvancedDeduplicationManager!

    override func setUp() {
        super.setUp()
        deduplicationManager = AdvancedDeduplicationManager()
    }

    func testDuplicateRequestsShareResponse() {
        let expectation = XCTestExpectation(description: "Both requests complete")
        expectation.expectedFulfillmentCount = 2

        let request = URLRequest(url: URL(string: "https://jsonplaceholder.typicode.com/posts/1")!)
        var responses: [Post] = []

        // Make two identical requests
        deduplicationManager.performRequest(request, responseType: Post.self) { result in
            if case .success(let post) = result {
                responses.append(post)
            }
            expectation.fulfill()
        }

        deduplicationManager.performRequest(request, responseType: Post.self) { result in
            if case .success(let post) = result {
                responses.append(post)
            }
            expectation.fulfill()
        }

        wait(for: [expectation], timeout: 10.0)

        // Both responses should be identical
        XCTAssertEqual(responses.count, 2)
        XCTAssertEqual(responses[0].id, responses[1].id)
    }
}

struct Post: Codable, Equatable {
    let id: Int
    let title: String
    let body: String
    let userId: Int
}

Best Practices and Considerations

When implementing request deduplication with Alamofire:

  1. Memory Management: Implement proper cleanup mechanisms to prevent memory leaks from accumulated cached responses and pending request callbacks.

  2. Cache Invalidation: Set appropriate cache timeouts and provide methods to manually clear cached responses when needed.

  3. Error Handling: Ensure that failed requests don't prevent future requests to the same endpoint from being executed.

  4. Thread Safety: Use concurrent queues with barrier flags to ensure thread-safe access to shared data structures.

  5. Request Uniqueness: Consider all relevant request parameters when creating unique keys, including headers, HTTP methods, and request bodies.

Performance Benefits

Request deduplication provides several performance advantages:

  • Reduced Network Traffic: Eliminates redundant API calls, saving bandwidth and reducing server load
  • Improved Response Times: Cached responses are delivered immediately without network latency
  • Better User Experience: Prevents UI flickering and inconsistent states caused by multiple simultaneous requests
  • Resource Conservation: Reduces CPU and memory usage associated with redundant network operations

By implementing request deduplication in your Alamofire-based applications, you can significantly improve performance and create a more efficient networking layer. The techniques shown above provide a solid foundation that can be adapted to your specific use cases and requirements.

Try WebScraping.AI for Your Web Scraping Needs

Looking for a powerful web scraping solution? WebScraping.AI provides an LLM-powered API that combines Chromium JavaScript rendering with rotating proxies for reliable data extraction.

Key Features:

  • AI-powered extraction: Ask questions about web pages or extract structured data fields
  • JavaScript rendering: Full Chromium browser support for dynamic content
  • Rotating proxies: Datacenter and residential proxies from multiple countries
  • Easy integration: Simple REST API with SDKs for Python, Ruby, PHP, and more
  • Reliable & scalable: Built for developers who need consistent results

Getting Started:

Get page content with AI analysis:

curl "https://api.webscraping.ai/ai/question?url=https://example.com&question=What is the main topic?&api_key=YOUR_API_KEY"

Extract structured data:

curl "https://api.webscraping.ai/ai/fields?url=https://example.com&fields[title]=Page title&fields[price]=Product price&api_key=YOUR_API_KEY"

Try in request builder

Related Questions

Get Started Now

WebScraping.AI provides rotating proxies, Chromium rendering and built-in HTML parser for web scraping
Icon