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-Authorizationheader — add a Basic-auth header to every request before it leaves the device. - Challenge-response via
urlSession(_:didReceive:)— implement theURLSessionDelegatemethod 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
Keychainor fetch credentials from your backend at runtime. - Proxy credentials in the
Proxy-Authorizationheader 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.defaultrespects the system's VPN and proxy settings. Using a customconnectionProxyDictionaryoverrides this, which is what you want for explicit proxy routing.
Proxy Type Comparison
| Feature | Residential | Mobile | Datacenter |
|---|---|---|---|
| IP source | ISP-assigned | Cellular carrier | Cloud / hosting provider |
| Detection difficulty | Hard | Very hard | Easy |
| Typical latency | 200–800ms | 300–1000ms | 50–200ms |
| Best for | Scraping, SERP, price monitoring | Social media, app store research | High-volume, low-friction APIs |
| Cost | Medium | High | Low |
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/HTTPSEnableto1(Int), nottrue(Bool), or the proxy is silently ignored.- Don't rely on
kCFProxyUsernameKey/kCFProxyPasswordKey— use aProxy-Authorizationheader 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 port1080.- Use
ThrowingTaskGroupfor 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.






