Using Proxies in Kotlin: A Code-First Guide with Ktor and OkHttp

Learn how to route Kotlin HTTP traffic through residential proxies using Ktor Client and OkHttp. Covers proxy auth, geo-targeting, SOCKS5, coroutine fan-out, and production hardening for Kotlin web scraping.

Using Proxies in Kotlin: A Code-First Guide with Ktor and OkHttp
In this article

Using proxies in Kotlin is a foundational skill for any developer building scrapers, automation pipelines, or mobile apps that need to fetch data from sources that restrict access by IP or ASN. Kotlin's coroutine-friendly ecosystem, combined with mature HTTP clients like Ktor and OkHttp, makes it straightforward to route requests through a proxy gateway, encode geo-targeting and sticky sessions in the username, and fan out thousands of concurrent requests safely. This guide walks through the full stack: project setup, ProxyHat gateway integration, SOCKS5 configuration, coroutine concurrency patterns, and production hardening.

Why Using Proxies in Kotlin Matters

Most modern web targets — e-commerce platforms, social networks, SERP endpoints, ticketing sites — deploy anti-bot systems that fingerprint connection metadata. A significant portion of these systems maintain blocklists of datacenter Autonomous System Numbers (ASNs). If your requests originate from a cloud provider like AWS, GCP, or DigitalOcean, there's a high chance the target will return a 403 or an interstitial CAPTCHA before your logic even runs. Residential proxies solve this by assigning you an IP that belongs to a real ISP subscriber, making your traffic indistinguishable from a normal user's browser session. For Kotlin web scraping workloads, this is often the difference between a 20% success rate and a 95%+ success rate.

The problem is particularly acute for mobile and social targets. Instagram, TikTok, and similar platforms aggressively rate-limit and block datacenter ranges. Mobile proxies — IPs assigned to cellular carriers — offer an even stronger trust signal because they match the network profile of the platform's actual user base. For backend Kotlin services doing SERP tracking or price monitoring, residential proxies are usually the right starting point; mobile proxies are reserved for the hardest targets.

Project Setup: Ktor Client and OkHttp Baseline

Assume a Gradle Kotlin DSL project. Add Ktor 3 (with the CIO or OkHttp engine) and OkHttp as dependencies. The OkHttp engine for Ktor delegates to OkHttp under the hood, so you get OkHttp's connection pooling and proxy handling while keeping Ktor's coroutine-native API.

// build.gradle.kts
plugins {
    kotlin("jvm") version "2.0.21"
    application
}

repositories { mavenCentral() }

val ktorVersion = "3.0.3"

dependencies {
    implementation("io.ktor:ktor-client-core:$ktorVersion")
    implementation("io.ktor:ktor-client-cio:$ktorVersion")
    implementation("io.ktor:ktor-client-okhttp:$ktorVersion")
    implementation("io.ktor:ktor-client-content-negotiation:$ktorVersion")
    implementation("io.ktor:ktor-serialization-kotlinx-json:$ktorVersion")
    implementation("com.squareup.okhttp3:okhttp:4.12.0")
    implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.9.0")
}

The raw OkHttp baseline below establishes a java.net.Proxy and an Authenticator that supplies Proxy-Authorization on 407 challenges. This is the lowest-level pattern; Ktor wraps it but the mechanics are the same.

// Raw OkHttp baseline — explicit Proxy + Authenticator
import okhttp3.OkHttpClient
import okhttp3.Request
import java.net.InetSocketAddress
import java.net.Proxy
import java.net.Authenticator
import java.net.PasswordAuthentication
import java.util.Base64

val proxyUser = "user-country-DE-city-berlin"
val proxyPass = "your-password"

// Configure the global default Authenticator for java.net.Proxy
Authenticator.setDefault(object : Authenticator() {
    override fun getPasswordAuthentication(): PasswordAuthentication {
        return PasswordAuthentication(proxyUser, proxyPass.toCharArray())
    }
})

val proxy = Proxy(Proxy.Type.HTTP, InetSocketAddress("gate.proxyhat.com", 8080))

val client = OkHttpClient.Builder()
    .proxy(proxy)
    .proxyAuthenticator { _, response ->
        val credential = Credentials.basic(proxyUser, proxyPass)
        response.request.newBuilder()
            .header("Proxy-Authorization", credential)
            .build()
    }
    .connectTimeout(10, java.util.concurrent.TimeUnit.SECONDS)
    .readTimeout(30, java.util.concurrent.TimeUnit.SECONDS)
    .build()

val request = Request.Builder()
    .url("https://httpbin.org/ip")
    .build()

client.newCall(request).execute().use { response ->
    println(response.code)
    println(response.body?.string())
}

Note the username encoding: user-country-DE-city-berlin tells the ProxyHat gateway to assign a residential IP geographically located in Berlin, Germany. This is how geo-targeting works on the ProxyHat gateway — the metadata rides in the proxy username, not in a query parameter or header. The ProxyHat documentation describes the full flag grammar.

Routing Through gate.proxyhat.com:8080 with Ktor

Ktor's proxy support is engine-specific. The OkHttp engine reads java.net.Proxy from the engine configuration; the CIO engine reads system properties. The cleanest cross-engine approach is to set the proxy in the engine config and inject the Proxy-Authorization header in defaultRequest, because Ktor does not automatically add proxy auth headers for you.

// Ktor 3 with OkHttp engine — proxy + auth via defaultRequest
import io.ktor.client.*
import io.ktor.client.engine.okhttp.*
import io.ktor.client.plugins.*
import io.ktor.client.request.*
import io.ktor.client.statement.*
import java.net.InetSocketAddress
import java.net.Proxy
import java.util.Base64

val proxyUser = "user-country-DE-city-berlin-session-abc123"
val proxyPass = "your-password"

val proxyHeader = "Basic " + Base64.getEncoder()
    .encodeToString("$proxyUser:$proxyPass".toByteArray())

val client = HttpClient(OkHttp) {
    engine {
        proxy = Proxy(Proxy.Type.HTTP, InetSocketAddress("gate.proxyhat.com", 8080))
    }
    defaultRequest {
        header("Proxy-Authorization", proxyHeader)
    }
}

suspend fun main() {
    val response: HttpResponse = client.get("https://httpbin.org/ip")
    println(response.status)
    println(response.bodyAsText())
    client.close()
}

The -session-abc123 suffix requests a sticky session: the gateway pins a specific residential IP to that session ID for a window (typically 10–30 minutes depending on provider policy). Without a session flag, every request rotates to a new IP. Use sticky sessions when you need login continuity or when a target ties cookies to IP; use rotating mode for bulk fetching where each request is independent.

Geo-targeting and Sticky Session Username Grammar

  • user-country-US — any US residential IP.
  • user-country-DE-city-berlin — city-level targeting in Germany.
  • user-country-GB-session-xyz789 — UK IP, sticky session xyz789.
  • user-country-DE-city-berlin-session-abc123 — combined city + sticky.

Flags are concatenated with hyphens. The gateway parses them left to right. See the ProxyHat locations page for the full list of supported countries and cities.

SOCKS5 on Port 1080 via System Properties

For SOCKS5, the ProxyHat gateway listens on port 1080. The JVM's SOCKS proxy support is wired through java.net.socks.* system properties, which both OkHttp and Ktor's CIO engine respect. SOCKS5 is useful when you need TCP-level tunneling for non-HTTP protocols or when an intermediary firewall strips HTTP proxy headers.

// SOCKS5 via system properties — works with CIO and OkHttp engines
import io.ktor.client.*
import io.ktor.client.engine.cio.*
import io.ktor.client.request.*
import io.ktor.client.statement.*

fun configureSocks5Proxy(user: String, pass: String) {
    System.setProperty("socksProxyHost", "gate.proxyhat.com")
    System.setProperty("socksProxyPort", "1080")
    System.setProperty("java.net.socks.username", user)
    System.setProperty("java.net.socks.password", pass)
}

suspend fun main() {
    configureSocks5Proxy("user-country-US-session-sock1", "your-password")

    val client = HttpClient(CIO) {
        engine {
            // CIO honors the socks.* system properties for proxying.
        }
    }

    val resp: HttpResponse = client.get("https://httpbin.org/ip")
    println(resp.bodyAsText())
    client.close()
}

System properties are global to the JVM, which makes this approach unsuitable for multi-tenant services that need per-request proxy selection. For fine-grained control, prefer the OkHttp engine with explicit Proxy objects per client instance, or instantiate separate HttpClient instances per proxy configuration.

Coroutine Fan-Out: Concurrent Requests with Rate Control

The reason Kotlin is compelling for scraping is coroutines: structured concurrency with async/awaitAll lets you fan out hundreds of requests with deterministic cancellation and backpressure. The pattern below uses a Semaphore to cap concurrent in-flight requests, preventing your proxy provider's rate limit from tripping and avoiding overwhelming the target.

// Coroutine fan-out with Semaphore-based rate control
import io.ktor.client.*
import io.ktor.client.engine.okhttp.*
import io.ktor.client.plugins.*
import io.ktor.client.request.*
import io.ktor.client.statement.*
import kotlinx.coroutines.*
import java.net.InetSocketAddress
import java.net.Proxy
import java.util.Base64

val targets = listOf(
    "https://httpbin.org/anything?id=1",
    "https://httpbin.org/anything?id=2",
    "https://httpbin.org/anything?id=3",
    "https://httpbin.org/anything?id=4",
    "https://httpbin.org/anything?id=5"
)

suspend fun fetchAll(client: HttpClient, urls: List<String>, concurrency: Int): List<String> = coroutineScope {
    val sem = Semaphore(concurrency)
    urls.map { url ->
        async {
            sem.withPermit {
                runCatching {
                    client.get(url).bodyAsText()
                }.getOrElse { e ->
                    "ERROR: ${e.message}"
                }
            }
        }
    }.awaitAll()
}

suspend fun main() {
    val proxyUser = "user-country-US"
    val proxyPass = "your-password"
    val proxyHeader = "Basic " + Base64.getEncoder()
        .encodeToString("$proxyUser:$proxyPass".toByteArray())

    val client = HttpClient(OkHttp) {
        engine {
            proxy = Proxy(Proxy.Type.HTTP, InetSocketAddress("gate.proxyhat.com", 8080))
        }
        defaultRequest { header("Proxy-Authorization", proxyHeader) }
    }

    val results = fetchAll(client, targets, concurrency = 5)
    results.forEachIndexed { i, r -> println("$i -> $r") }
    client.close()
}

Key production points for the fan-out pattern:

  • Semaphore, not unbounded async: a 1000-element list with no semaphore will spawn 1000 concurrent socket attempts and trip proxy rate limits. Cap concurrency to a value your provider supports — ProxyHat residential plans commonly allow 100+ concurrent sessions; check your plan on the pricing page.
  • runCatching per request: one failure should not cancel the whole batch. awaitAll propagates the first exception, so wrap each call.
  • Structured concurrency: coroutineScope ensures all children complete before returning. If the parent scope is cancelled, all in-flight requests are cancelled cleanly.
  • Rotate sessions per request: for true IP rotation, generate a unique session ID per request (e.g., user-country-US-session-${UUID.randomUUID()}) so each call gets a fresh exit IP.

Production Hardening

OkHttp Authenticator for 407 Challenges

Some proxy gateways issue a 407 Proxy Authentication Required challenge on the first connection rather than accepting preemptive auth. OkHttp's proxyAuthenticator handles this by retrying with credentials. Ktor's OkHttp engine exposes the underlying builder via engine { config { ... } }, so you can set the authenticator there.

// Ktor OkHttp engine with proxyAuthenticator + retries + timeouts
import io.ktor.client.*
import io.ktor.client.engine.okhttp.*
import io.ktor.client.plugins.*
import okhttp3.Credentials
import java.net.InetSocketAddress
import java.net.Proxy
import java.util.concurrent.TimeUnit

fun buildHardenedClient(user: String, pass: String): HttpClient {
    val proxy = Proxy(Proxy.Type.HTTP, InetSocketAddress("gate.proxyhat.com", 8080))
    return HttpClient(OkHttp) {
        engine {
            this.proxy = proxy
            config {
                proxyAuthenticator { _, response ->
                    response.request.newBuilder()
                        .header("Proxy-Authorization", Credentials.basic(user, pass))
                        .build()
                }
                connectTimeout(10, TimeUnit.SECONDS)
                readTimeout(30, TimeUnit.SECONDS)
                writeTimeout(15, TimeUnit.SECONDS)
                retryOnConnectionFailure(true)
                // Connection pool tuning
                connectionPool(okhttp3.ConnectionPool(50, 5, TimeUnit.MINUTES))
            }
        }
        defaultRequest {
            header("Proxy-Authorization", Credentials.basic(user, pass))
            header("User-Agent", "Mozilla/5.0 (compatible; MyApp/1.0)")
        }
    }
}

The proxyAuthenticator is the robust path: even if the preemptive header in defaultRequest is stripped by an intermediary, OkHttp will respond to the 407 and retry once with credentials. The connectionPool(50, 5, ...) keeps 50 idle connections alive for 5 minutes, reducing TLS handshake overhead on repeated requests to the same host.

TLS Configuration

For scraping HTTPS targets, the default JVM trust store is usually sufficient. If you need to pin certificates or customize trust managers, configure the sslSocketFactory on the OkHttp builder. Avoid disabling certificate verification in production — it exposes your traffic to MITM and breaks the trust model that makes residential proxies effective in the first place.

Android NetworkSecurityConfig

On Android, cleartext traffic is blocked by default since API 28. If your proxy gateway uses HTTP (not HTTPS) for the CONNECT tunnel — which is normal for proxy protocols — Android's network_security_config.xml must allow cleartext to the proxy host specifically. Add a <domain-config cleartextTrafficPermitted="true"> entry for gate.proxyhat.com. Do not enable cleartext globally; that defeats the platform's protection. The Android Network Security Configuration guide documents the schema.

Retries and Circuit Breakers

For long-running scraping jobs, implement a retry policy with exponential backoff and jitter. A simple pattern: retry up to 3 times with delays of 500ms, 1s, 2s plus a random jitter of 0–200ms. For more sophisticated setups, consider a circuit breaker that trips after N consecutive failures and pauses new requests for a cooldown period — this prevents burning proxy credits against a target that is currently blocking you. The Resilience4j library integrates cleanly with Kotlin coroutines via retry { ... } and circuitBreaker { ... } suspend wrappers.

Proxy access is a technical capability, not a legal waiver. In the United States, the Computer Fraud and Abuse Act (CFAA) and analogous state laws can apply to accessing systems in ways that violate terms of service, especially when circumventing technical access controls. In the EU, the GDPR governs the collection and processing of personal data, including data that is technically public but identifies individuals. Always:

  • Prefer official APIs when available. They are cheaper, more reliable, and legally safer than scraping.
  • Respect robots.txt and the target's stated crawl rate.
  • Collect only public data necessary for your use case. Avoid scraping personal data without a lawful basis.
  • Honor takedown requests from data subjects.

The ProxyHat SDK mirrors the patterns in this article: it wraps the gateway connection, encodes geo/session flags in the username, and exposes both HTTP and SOCKS5 transports. The SDK is a convenience layer; the underlying mechanics are exactly what you have seen here — gate.proxyhat.com:8080 for HTTP, :1080 for SOCKS5, and username-encoded metadata.

Key Takeaways

  • Use the OkHttp engine in Ktor for the most reliable proxy handling; set Proxy in the engine config and inject Proxy-Authorization in defaultRequest.
  • Encode geo-targeting (user-country-DE-city-berlin) and sticky sessions (-session-abc123) in the username — not in headers or query params.
  • SOCKS5 on port 1080 works via java.net.socks.* system properties, but they are JVM-global; use explicit Proxy objects for per-request control.
  • Cap concurrency with a Semaphore; unbounded async will trip rate limits and burn proxy credits.
  • Always add an OkHttp proxyAuthenticator to handle 407 challenges, even if you also send preemptive auth.
  • Prefer official APIs and respect robots.txt and ToS. Residential proxies are a tool, not a license to bypass legal constraints.

For pricing and plan details, see the ProxyHat pricing page. For the full list of supported geos, browse locations. The patterns in this guide apply equally to Android apps and JVM backend services — the only Android-specific note is NetworkSecurityConfig for cleartext to the proxy host.

Frequently asked questions

What is Using Proxies in Kotlin?

Using proxies in Kotlin means routing HTTP or SOCKS5 traffic from a Kotlin application — typically via Ktor Client or OkHttp — through an intermediary gateway that assigns a different exit IP per request. The proxy credentials, geo-targeting flags, and session identifiers are encoded in the username, and the gateway handles IP rotation, authentication, and TLS termination to the target. This is the standard pattern for Kotlin web scraping and automated data collection.

Why does Using Proxies in Kotlin matter for proxy users?

Kotlin's coroutine model makes concurrent HTTP requests cheap and structured, but most web targets block datacenter ASNs and rate-limit by IP. Routing through residential or mobile proxies lets a Kotlin service present a real-ISP IP per request, dramatically improving success rates on social, e-commerce, and SERP targets. Without a proxy, a Kotlin scraper hitting a protected target will see 403s and CAPTCHAs within dozens of requests.

Which proxy type works best for Using Proxies in Kotlin?

Residential proxies are the best default for Kotlin web scraping because they carry real ISP metadata and bypass most datacenter ASN blocklists. Mobile proxies are stronger for the hardest social targets like Instagram or TikTok but cost more. Datacenter proxies are fast and cheap but are blocked by many modern anti-bot systems. Start with residential for general scraping; escalate to mobile only when residential is being blocked.

How do you avoid blocks when implementing Using Proxies in Kotlin?

Use rotating residential IPs with a unique session ID per request, cap concurrency with a Semaphore to respect rate limits, set a realistic User-Agent and Accept headers, add exponential backoff with jitter on failures, and use an OkHttp proxyAuthenticator to handle 407 challenges. Also respect robots.txt and prefer official APIs where available — proxies do not exempt you from a target's terms of service or from laws like the CFAA and GDPR.

Verify your proxy setup in seconds

Free proxy checker — confirm your IPs are fast, anonymous and unblocked.

Check proxies free
← Back to Blog