Using Proxies in Swift: A Code-First URLSession Guide for iOS & macOS

Learn how to configure residential, mobile, and datacenter proxies in Swift using URLSession. Covers HTTP/HTTPS proxy dictionaries, SOCKS5, authentication, geo-targeting, async/await concurrency, and production best practices.

Using Proxies in Swift: A Code-First URLSession Guide for iOS & macOS

Why Using Proxies in Swift Matters

Using proxies in Swift is essential whenever your iOS or macOS app needs to reach endpoints that restrict datacenter IPs, serve region-locked content, or enforce rate limits per IP. Apple's URLSession stack exposes proxy configuration through URLSessionConfiguration.connectionProxyDictionary, but the documentation is sparse and several authentication keys are unreliable on modern iOS versions. This guide walks you through every piece you need — from a basic HTTP proxy dictionary to a production-grade async/await scraper with retries and TLS handling.

Throughout this article we'll use ProxyHat as the proxy gateway (gate.proxyhat.com:8080 for HTTP, :1080 for SOCKS5). The same gateway works with the ProxyHat Python and Node SDKs, so your server-side components can mirror the on-device logic. See the ProxyHat documentation for full reference.

Technical Context: How URLSession Handles Proxies

Apple's networking stack is built on CFNetwork, which exposes proxy settings through a dictionary of kCFNetworkProxies* keys. You attach this dictionary to a URLSessionConfiguration before creating the URLSession. The keys are defined in CFNetwork and map to Apple's CFNetwork framework.

The critical problem is authentication. While the keys kCFProxyUsernameKey and kCFProxyPasswordKey exist in the CFNetwork header, URLSession frequently ignores them on iOS 15+ and macOS 12+. The reliable approaches are:

  • Pre-emptive Proxy-Authorization header — add a Basic-auth header to every request before it leaves the device.
  • Challenge-response via urlSession(_:didReceive:) — implement the URLSessionDelegate method to respond to HTTP 407 challenges.

For SOCKS5, you use a parallel set of kCFStreamPropertySOCKSProxy* keys. Username/password for SOCKS5 is equally unreliable through the dictionary, so the same header or challenge approach applies.

Configuring an HTTP/HTTPS Proxy Dictionary

Let's start with a reusable function that builds a URLSessionConfiguration pointing at the ProxyHat gateway. We'll enable both HTTP and HTTPS proxy entries.

import Foundation

func makeProxyConfiguration(host: String, port: Int) -> URLSessionConfiguration {
    let config = URLSessionConfiguration.default
    config.connectionProxyDictionary = [
        kCFNetworkProxiesHTTPEnable: 1,
        kCFNetworkProxiesHTTPProxy: host,
        kCFNetworkProxiesHTTPPort: port,
        kCFNetworkProxiesHTTPSEnable: 1,
        kCFNetworkProxiesHTTPSProxy: host,
        kCFNetworkProxiesHTTPSPort: port
    ]
    // Prevent the session from falling back to system proxy settings
    config.connectionProxyDictionary?[kCFProxyTypeKey] = kCFProxyTypeHTTP
    return config
}

let proxyConfig = makeProxyConfiguration(host: "gate.proxyhat.com", port: 8080)
let session = URLSession(configuration: proxyConfig)

The kCFNetworkProxiesHTTPEnable and kCFNetworkProxiesHTTPSEnable keys must be set to 1 (an Int, not a Bool) or the proxy is silently ignored. This is a common pitfall that costs hours of debugging.

Authentication and Geo-Targeting

Option A: Pre-emptive Proxy-Authorization Header

ProxyHat encodes geo-targeting and session stickiness in the username string. For example, user-country-US-city-newyork-session-abc123 routes requests through a New York residential IP and keeps the same exit IP for the session abc123. Since URLSession won't reliably pass proxy credentials from the dictionary, we compute a Basic-auth header ourselves.

import Foundation

func makeProxyAuthHeader(username: String, password: String) -> String {
    let credentials = "\(username):\(password)"
    let encoded = Data(credentials.utf8).base64EncodedString()
    return "Basic \(encoded)"
}

func makeProxiedRequest(url: URL, country: String, city: String, session: String) -> URLRequest {
    var request = URLRequest(url: url, timeoutInterval: 30)
    // ProxyHat username encodes geo + session flags
    let username = "user-country-\(country)-city-\(city)-session-\(session)"
    let password = "YOUR_PROXYHAT_PASSWORD"
    request.setValue(
        makeProxyAuthHeader(username: username, password: password),
        forHTTPHeaderField: "Proxy-Authorization"
    )
    return request
}

let url = URL(string: "https://httpbin.org/ip")!
let request = makeProxiedRequest(
    url: url, country: "US", city: "newyork", session: "abc123"
)
let task = session.dataTask(with: request) { data, response, error in
    if let error { print("Error: \(error)"); return }
    if let data, let body = String(data: data, encoding: .utf8) {
        print("Response: \(body)")
    }
}
task.resume()

Option B: Challenge-Response Delegate

Alternatively, implement urlSession(_:didReceive:completionHandler:) to handle the 407 challenge. This is more robust if an intermediate proxy strips your pre-emptive header.

final class ProxyAuthDelegate: NSObject, URLSessionDelegate {
    let username: String
    let password: String

    init(username: String, password: String) {
        self.username = username
        self.password = password
    }

    func urlSession(
        _ session: URLSession,
        didReceive challenge: URLAuthenticationChallenge,
        completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void
    ) {
        guard challenge.protectionSpace.authenticationMethod ==
                NSURLAuthenticationMethodHTTPProxy else {
            completionHandler(.performDefaultHandling, nil)
            return
        }
        let credential = URLCredential(
            user: username, password: password, persistence: .forSession
        )
        completionHandler(.useCredential, credential)
    }
}

let delegate = ProxyAuthDelegate(
    username: "user-country-DE-city-berlin-session-xyz789",
    password: "YOUR_PROXYHAT_PASSWORD"
)
let authedSession = URLSession(
    configuration: proxyConfig,
    delegate: delegate,
    delegateQueue: nil
)

Check the ProxyHat locations page for the full list of supported countries and cities. You can also review pricing plans to match traffic volume to your use case.

SOCKS5 Proxy on Port 1080

For SOCKS5, swap the HTTP keys for the kCFStreamPropertySOCKSProxy* family and point at port 1080.

import Foundation

func makeSOCKS5Configuration(host: String, port: Int) -> URLSessionConfiguration {
    let config = URLSessionConfiguration.default
    config.connectionProxyDictionary = [
        kCFProxyTypeKey: kCFProxyTypeSOCKS,
        kCFStreamPropertySOCKSProxyHost: host,
        kCFStreamPropertySOCKSProxyPort: port,
        kCFStreamPropertySOCKSVersion: kCFStreamSocketSOCKSVersion5
    ]
    return config
}

let socksConfig = makeSOCKS5Configuration(host: "gate.proxyhat.com", port: 1080)
let socksSession = URLSession(configuration: socksConfig)

// Authentication still needs the Proxy-Authorization header
// or a challenge-response delegate — same as HTTP above.

SOCKS5 can be preferable when you need TCP-level tunneling for non-HTTP protocols or when an upstream firewall blocks HTTP CONNECT. However, URLSession always speaks HTTP(S) over the tunnel, so the practical difference for most apps is minimal.

Residential Proxies for Blocked Endpoints and Region-Locked Content

Datacenter IPs are trivially detected by services like Apple's URLSession documentation references and anti-bot providers. Residential proxies route through real ISP-assigned IPs, making them essential for:

  • App endpoints that block cloud IP ranges — many e-commerce and ticketing APIs return 403 for AWS/GCP/Azure CIDR blocks.
  • Region-locked content — streaming metadata, localized pricing, and SERP results vary by geography.
  • Rate-limited APIs — rotating residential IPs lets you stay under per-IP thresholds.

For a deeper look at scraping use cases, see our web scraping use case and SERP tracking use case.

Async/Await with Codable and TaskGroup Concurrency

Now let's build a complete async example. We'll fetch JSON from multiple endpoints concurrently using a ThrowingTaskGroup, decode with Codable, and route each request through a different geo-targeted residential IP.

import Foundation

struct IPResponse: Codable {
    let origin: String
}

struct ProxySettings {
    let country: String
    let city: String
    let sessionID: String
    let password: String
}

actor Scraper {
    let session: URLSession
    let password: String

    init(password: String) {
        let config = makeProxyConfiguration(host: "gate.proxyhat.com", port: 8080)
        self.session = URLSession(configuration: config)
        self.password = password
    }

    func fetchIP(settings: ProxySettings) async throws -> IPResponse {
        let url = URL(string: "https://httpbin.org/ip")!
        var request = URLRequest(url: url, timeoutInterval: 30)
        let username = "user-country-\(settings.country)-city-\(settings.city)-session-\(settings.sessionID)"
        request.setValue(
            makeProxyAuthHeader(username: username, password: settings.password),
            forHTTPHeaderField: "Proxy-Authorization"
        )

        let (data, response) = try await session.data(for: request)
        guard let http = response as? HTTPURLResponse,
              (200...299).contains(http.statusCode) else {
            throw URLError(.badServerResponse)
        }
        return try JSONDecoder().decode(IPResponse.self, from: data)
    }

    func fetchMultiple() async throws -> [IPResponse] {
        let settingsList = [
            ProxySettings(country: "US", city: "newyork", sessionID: "s1", password: password),
            ProxySettings(country: "DE", city: "berlin", sessionID: "s2", password: password),
            ProxySettings(country: "JP", city: "tokyo", sessionID: "s3", password: password),
            ProxySettings(country: "GB", city: "london", sessionID: "s4", password: password),
            ProxySettings(country: "FR", city: "paris", sessionID: "s5", password: password)
        ]

        return try await withThrowingTaskGroup(of: IPResponse.self) { group in
            for settings in settingsList {
                group.addTask { try await self.fetchIP(settings: settings) }
            }
            var results: [IPResponse] = []
            for try await result in group {
                results.append(result)
            }
            return results
        }
    }
}

// Usage
Task {
    let scraper = Scraper(password: "YOUR_PROXYHAT_PASSWORD")
    do {
        let ips = try await scraper.fetchMultiple()
        for ip in ips { print("Exit IP: \(ip.origin)") }
    } catch {
        print("Scraping failed: \(error)")
    }
}

This example fires 5 concurrent requests, each through a different country/city exit, and collects the results. With residential proxies you can realistically sustain 50–100 concurrent sessions per ProxyHat plan tier — check your plan's concurrency limits on the pricing page.

Production Tips

TLS Handling with URLSessionDelegate

If you need custom TLS trust evaluation (for example, pinning certificates or debugging certificate issues through the proxy), implement urlSession(_:didReceive:completionHandler:) for the serverTrust challenge.

final class TLSPinningDelegate: NSObject, URLSessionDelegate {
    func urlSession(
        _ session: URLSession,
        didReceive challenge: URLAuthenticationChallenge,
        completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void
    ) {
        if challenge.protectionSpace.authenticationMethod == NSURLAuthenticationMethodServerTrust,
           let trust = challenge.protectionSpace.serverTrust {
            // In production, validate the certificate chain here.
            // For now, we accept the system default.
            completionHandler(.useCredential, URLCredential(trust: trust))
        } else {
            completionHandler(.performDefaultHandling, nil)
        }
    }
}

Retry with Exponential Backoff

Network failures and transient 429/503 responses are inevitable. Wrap your fetch in a retry loop with exponential backoff and jitter.

func fetchWithRetry<T: Decodable>(
    request: URLRequest,
    session: URLSession,
    maxRetries: Int = 3,
    baseDelay: UInt64 = 1_000_000_000  // 1 second in nanoseconds
) async throws -> T {
    var attempt = 0
    while true {
        do {
            let (data, response) = try await session.data(for: request)
            if let http = response as? HTTPURLResponse,
               http.statusCode == 429 || http.statusCode >= 500 {
                throw URLError(.badServerResponse)
            }
            return try JSONDecoder().decode(T.self, from: data)
        } catch {
            attempt += 1
            if attempt >= maxRetries { throw error }
            let jitter = UInt64.random(in: 0...500_000_000)
            let delay = baseDelay * UInt64(1 << (attempt - 1)) + jitter
            try await Task.sleep(nanoseconds: delay)
        }
    }
}

App Transport Security (ATS)

ATS requires TLS 1.2+ for all connections. Proxy connections are transparent to ATS — the TLS handshake happens between your app and the target server, not between your app and the proxy. However, if you connect to an HTTP (non-HTTPS) endpoint through the proxy, you must add an ATS exception in Info.plist:

<key>NSAppTransportSecurity</key>
<dict>
  <key>NSAllowsArbitraryLoads</key>
  <true/>
</dict>

Prefer HTTPS endpoints whenever possible to avoid weakening your app's security posture.

On-Device Privacy Notes

  • Never hardcode proxy credentials in your app binary. Use Keychain or fetch credentials from your backend at runtime.
  • Proxy credentials in the Proxy-Authorization header are visible to your app's process — not to the end user — but they are visible in crash logs if you log request headers. Strip sensitive headers from logs.
  • On iOS, URLSessionConfiguration.default respects the system's VPN and proxy settings. Using a custom connectionProxyDictionary overrides this, which is what you want for explicit proxy routing.

Proxy Type Comparison

FeatureResidentialMobileDatacenter
IP sourceISP-assignedCellular carrierCloud / hosting provider
Detection difficultyHardVery hardEasy
Typical latency200–800ms300–1000ms50–200ms
Best forScraping, SERP, price monitoringSocial media, app store researchHigh-volume, low-friction APIs
CostMediumHighLow

Ethics and Legal Considerations

Proxy access is a tool, not a license to bypass terms of service. Key considerations:

  • Prefer official APIs. If a service offers an API — even a paid one — use it. Scraping should be a last resort for legitimate, public data.
  • US: CFAA. The Computer Fraud and Abuse Act criminalizes unauthorized access to protected systems. Courts have generally held that accessing public web pages is not a CFAA violation, but circumventing authentication or rate limits can be. See Wikipedia's CFAA overview for background.
  • EU: GDPR. If you collect personal data (names, emails, user IDs), GDPR applies regardless of where your servers are. Ensure a lawful basis for processing.
  • App Store Guidelines. Apple's App Store Review Guidelines require apps to respect applicable laws. Apps that scrape third-party services without authorization risk rejection or removal.
  • robots.txt. Respect crawl-delay directives and disallow rules. It's not legally binding in most jurisdictions, but it's a good-faith signal.

Key Takeaways

  • Set kCFNetworkProxiesHTTPEnable / HTTPSEnable to 1 (Int), not true (Bool), or the proxy is silently ignored.
  • Don't rely on kCFProxyUsernameKey / kCFProxyPasswordKey — use a Proxy-Authorization header or a challenge-response delegate.
  • Encode geo-targeting and session stickiness in the ProxyHat username: user-country-US-city-newyork-session-abc123.
  • SOCKS5 uses kCFStreamPropertySOCKSProxy* keys on port 1080.
  • Use ThrowingTaskGroup for concurrent multi-geo requests and exponential backoff for retries.
  • Store proxy credentials in Keychain, never in the app binary. Strip sensitive headers from crash logs.
  • Always prefer official APIs. Respect CFAA, GDPR, robots.txt, and App Store guidelines.

FAQ

What is using proxies in Swift?

Using proxies in Swift means routing URLSession traffic through an intermediate server by configuring URLSessionConfiguration.connectionProxyDictionary with CFNetwork keys like kCFNetworkProxiesHTTPProxy and kCFNetworkProxiesHTTPPort. This lets your iOS or macOS app appear to originate from a different IP address, which is useful for geo-testing, scraping public data, and avoiding per-IP rate limits.

Why does using proxies in Swift matter for proxy users?

Apple's URLSession proxy support is underdocumented and has unreliable credential handling — kCFProxyUsernameKey and kCFProxyPasswordKey are frequently ignored on iOS 15+. Understanding the correct configuration dictionary keys and the Proxy-Authorization header workaround is the difference between a working proxy setup and hours of silent failures. This matters especially for Swift web scraping and SERP tracking where residential IPs are required.

Which proxy type works best for using proxies in Swift?

Residential proxies are the best default for Swift apps that scrape public endpoints or access region-locked content, because they use real ISP IPs that are harder to detect. Mobile proxies offer even higher trust scores for social media and app store research but cost more. Datacenter proxies are fine for high-volume, low-friction APIs that don't block cloud IP ranges. SOCKS5 on port 1080 is useful when HTTP CONNECT is blocked upstream.

How do you avoid blocks when implementing using proxies in Swift?

Rotate residential IPs per request or use sticky sessions with short TTLs. Add a Proxy-Authorization header with geo-targeting flags like user-country-US-city-newyork-session-abc123. Implement retry with exponential backoff and jitter for 429/503 responses. Limit concurrency to 50–100 sessions to avoid triggering anti-bot heuristics. Always respect robots.txt and prefer official APIs when available.

Ready to get started?

Access 50M+ residential IPs across 148+ countries with AI-powered filtering.

View PricingResidential Proxies
← Back to Blog