OkHttp proxy: auth, SOCKS5, rotation and 407 fixes

OkHttp proxy setup in Kotlin and Java for OkHttp 4.12 and 5.x: proxyAuthenticator done right, SOCKS5 logins, the system proxy, rotation and the 407 traps.

Quick summary · TL;DR
  1. A Proxy plus a proxyAuthenticator is the whole setup. Pass Proxy(Proxy.Type.HTTP, InetSocketAddress(HOST, PORT)) to OkHttpClient.Builder.proxy() and return a request with a Proxy-Authorization header built by Credentials.basic from proxyAuthenticator. HTTPS targets then go through a CONNECT tunnel.
  2. Interceptors never reach the CONNECT. A Proxy-Authorization header added in an interceptor works for http:// URLs and never for https:// URLs, which explains "Failed to authenticate with proxy" with the right password.
  3. Guard the authenticator. Return null when the request already carries Proxy-Authorization. Without that check a wrong password costs 21 tunnel attempts, or an endless loop against a proxy that keeps the connection open.
  4. SOCKS5 logins come from java.net.Authenticator. OkHttp hands SOCKS to the JDK, which asks the JVM-wide default Authenticator; proxyAuthenticator is never called. OkHttp sends the hostname to the SOCKS proxy, so DNS resolves there.

To set up an OkHttp proxy, pass a java.net.Proxy to OkHttpClient.Builder.proxy(), for example Proxy(Proxy.Type.HTTP, InetSocketAddress(HOST, PORT)), and give the builder a proxyAuthenticator that adds a Proxy-Authorization header built with Credentials.basic(USERNAME, PASSWORD). HTTPS targets then go through a CONNECT tunnel. For SOCKS5, use Proxy.Type.SOCKS and supply the login through java.net.Authenticator instead.

That snippet is on every page that ranks for an OkHttp proxy, and the trouble starts after it. IOException: Failed to authenticate with proxy arrives although the password is right, because the header was added in an interceptor. Copy an authenticator with no guard and it turns one wrong password into 21 failed logins, or into a loop that never ends. SOCKS5 logins fail because OkHttp never asks for credentials, and a rotating proxy hands out the same IP for a whole run.

Each of these comes from how OkHttp builds the tunnel. Every snippet below compiles against OkHttp 5.5.0 (released August 16, 2026) and, with the one body!! change noted under the first sample, the last 4.x release, 4.12.0. Every behaviour described traces to the OkHttp source at tag parent-5.5.0 (checked October 16, 2026).

OkHttp proxy setup in one answer

OkHttp takes the proxy from one of three places, and the first one set wins. An explicit proxy() on the builder is used for every call and nothing else is tried. Without it, OkHttp asks the builder’s proxySelector(). Without that, it asks the JVM’s ProxySelector.getDefault(), which reads system properties such as https.proxyHost on the JVM and follows the device proxy on Android.

Protocol follows the type. Proxy.Type.HTTP means plain HTTP to the proxy: OkHttp sends GET http://... for http:// URLs and a CONNECT host:443 for https:// URLs, then runs TLS end to end with the site inside the tunnel. There is no proxy URL and so no https:// scheme to get wrong. Proxy.Type.SOCKS means SOCKS5.

java.net.Proxy has no type for TLS between the client and the proxy itself, so OkHttp has no built-in option for it. Issue #8373 tracks that request and is still open; the maintainer calls the socket-factory workaround “clumsy”. For https:// targets, URLs, cookies and page content stay inside the end-to-end TLS session with the site either way; the proxy sees the target host from the CONNECT line.

One build note for OkHttp 5. It is published as a Kotlin Multiplatform project, so Gradle resolves com.squareup.okhttp3:okhttp:5.5.0 on its own, but in Maven the okhttp artifact is empty and the dependency must be okhttp-jvm (or okhttp-android), as the project README says. Any Maven build that resolves okhttp 5.x and then cannot find OkHttpClient has hit exactly this.

OkHttp proxy authentication

Credentials go on proxyAuthenticator, not on authenticator, which answers 401 challenges from the website. This is the Kotlin version for OkHttp 5:

import java.net.InetSocketAddress
import java.net.Proxy
import java.time.Duration
import okhttp3.Authenticator
import okhttp3.Credentials
import okhttp3.OkHttpClient
import okhttp3.Request

fun main() {
    val host = System.getenv("PROXY_HOST")
    val port = System.getenv("PROXY_PORT").toInt()
    val credential = Credentials.basic(System.getenv("PROXY_USER"), System.getenv("PROXY_PASS"))

    val proxyAuth = Authenticator { _, response ->
        if (response.request.header("Proxy-Authorization") != null) {
            null // credentials already sent and refused: give up
        } else {
            response.request.newBuilder()
                .header("Proxy-Authorization", credential)
                .build()
        }
    }

    val client = OkHttpClient.Builder()
        .proxy(Proxy(Proxy.Type.HTTP, InetSocketAddress(host, port)))
        .proxyAuthenticator(proxyAuth)
        .connectTimeout(Duration.ofSeconds(10))
        .readTimeout(Duration.ofSeconds(30))
        .build()

    val request = Request.Builder().url("https://api.ipify.org").build()
    client.newCall(request).execute().use { response ->
        println("${response.code} ${response.body.string()}")
    }
}

On OkHttp 4.x the body is nullable in Kotlin, so the last line reads response.body!!.string(); OkHttp 5 made Response.body non-null. The lambda form works on both, because Authenticator has been a Kotlin fun interface since 4.9.0. In Java, the same client reads:

import java.net.InetSocketAddress;
import java.net.Proxy;
import java.time.Duration;
import okhttp3.Credentials;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.Response;

public class OkHttpProxyDemo {
    public static void main(String[] args) throws Exception {
        String host = System.getenv("PROXY_HOST");
        int port = Integer.parseInt(System.getenv("PROXY_PORT"));
        String credential = Credentials.basic(System.getenv("PROXY_USER"), System.getenv("PROXY_PASS"));

        OkHttpClient client = new OkHttpClient.Builder()
                .proxy(new Proxy(Proxy.Type.HTTP, new InetSocketAddress(host, port)))
                .proxyAuthenticator((route, response) -> {
                    if (response.request().header("Proxy-Authorization") != null) {
                        return null; // credentials already sent and refused: give up
                    }
                    return response.request().newBuilder()
                            .header("Proxy-Authorization", credential)
                            .build();
                })
                .connectTimeout(Duration.ofSeconds(10))
                .readTimeout(Duration.ofSeconds(30))
                .build();

        Request request = new Request.Builder().url("https://api.ipify.org").build();
        try (Response response = client.newCall(request).execute()) {
            System.out.println(response.code() + " " + response.body().string());
        }
    }
}

For the JVM-wide setup that sits around this (system properties, the JDK clients, Apache HttpClient), see the Java proxy setup guide.

What OkHttp sends on the CONNECT

Preemptive by design. Before every CONNECT, OkHttp calls the proxy authenticator with a fake 407 Preemptive Authenticate response carrying a Proxy-Authenticate: OkHttp-Preemptive challenge, as the Authenticator documentation describes. The fake response’s request has no Proxy-Authorization header yet, so the guarded authenticator above returns credentials and they ride on the first CONNECT. Against a logging proxy, OkHttp 5.5.0 and 4.12.0 both opened the tunnel with a single CONNECT and no 407 round trip.

Plain http:// targets are reactive. No CONNECT means no preemptive call: the first GET http://... goes without credentials, the proxy answers 407, and OkHttp retries with the header.

Interceptors never see the CONNECT. The tunnel request “does not participate in any interceptors or event listeners”, in OkHttp’s own words, and it carries only Host, Proxy-Connection and OkHttp’s own User-Agent. Headers added in an application or network interceptor, Proxy-Authorization included, reach the proxy for http:// URLs and never for https:// URLs. That explains “Failed to authenticate with proxy” with the right password.

Authenticator.JAVA_NET_AUTHENTICATOR hands the challenge to the JVM’s default java.net.Authenticator. It works for Basic, but it only answers real challenges, so every new tunnel costs a 407 round trip first.

Fixing 407 and “Failed to authenticate”

java.io.IOException: Failed to authenticate with proxy means the proxy answered the CONNECT with 407 and the authenticator returned null. It usually comes down to no proxyAuthenticator at all (credentials in an interceptor, or on authenticator()), the guard firing because the proxy refused the credentials that were sent, or credentials read from the wrong variable. With the guard in place, a wrong password ends in exactly this exception after one refused CONNECT. For the same status code in curl, browsers and other languages, see 407 Proxy Authentication Required.

Both numbers come from ConnectPlan.kt. A proxy that sends Connection: close with its 407 forces a new connection per attempt, and MAX_TUNNEL_ATTEMPTS = 21 ends that with ProtocolException: Too many tunnel connections attempted: 21. If the proxy keeps the connection open instead, OkHttp goes around an inner while (true) loop on the same socket, and nothing counts those turns. Against a local test proxy that kept the connection open, an unguarded client on OkHttp 5.5.0 sent about 290,000 CONNECT requests in 30 seconds before it was killed. Every one of them is a refused login on the proxy.

For http:// targets the cap is the follow-up limit of RetryAndFollowUpInterceptor, 20 follow-ups, after which OkHttp throws Too many follow-up requests: 21. With the guard, a refused login on an http:// URL returns the 407 response to the caller.

OkHttp SOCKS5 proxy with a login

SOCKS5 support is a Proxy.Type.SOCKS, and the login cannot come from proxyAuthenticator. OkHttp creates the socket with java.net.Socket(proxy), so the JDK’s own SOCKS code runs the handshake, and that code asks the JVM-wide default java.net.Authenticator for the username and password. OkHttp’s authenticator is never called for SOCKS, and Credentials.basic builds an HTTP header that SOCKS has no place for.

import java.net.Authenticator
import java.net.InetSocketAddress
import java.net.PasswordAuthentication
import java.net.Proxy
import okhttp3.OkHttpClient
import okhttp3.Request

fun main() {
    val host = System.getenv("PROXY_HOST")
    val port = System.getenv("SOCKS_PORT").toInt()
    val user = System.getenv("PROXY_USER")
    val pass = System.getenv("PROXY_PASS").toCharArray()

    // The JDK's SOCKS code asks the JVM-wide default Authenticator, never OkHttp's.
    Authenticator.setDefault(object : Authenticator() {
        override fun getPasswordAuthentication(): PasswordAuthentication? =
            if (requestingProtocol == "SOCKS5" &&
                requestingHost.equals(host, ignoreCase = true) &&
                requestingPort == port
            ) PasswordAuthentication(user, pass) else null
    })

    val client = OkHttpClient.Builder()
        .proxy(Proxy(Proxy.Type.SOCKS, InetSocketAddress(host, port)))
        .build()

    val request = Request.Builder().url("https://api.ipify.org").build()
    client.newCall(request).execute().use { println("${it.code} ${it.body.string()}") }
}

Checking protocol, host and port keeps the proxy password away from any website that sends a 401 to some other part of the JVM. The JDK reports SOCKS logins with protocol "SOCKS5", per the SocksSocketImpl source, so an authenticator that checks only for RequestorType.PROXY returns nothing here. Refused logins surface as java.net.SocketException: SOCKS : authentication failed on JDK 21.

The hostname goes to the proxy. For a SOCKS route, OkHttp builds the target address with InetSocketAddress.createUnresolved(host, port) in RouteSelector.kt, so the client does no DNS lookup and the SOCKS5 request carries the domain name. In a logging SOCKS server, both 4.12.0 and 5.5.0 sent ATYP DOMAIN api.ipify.org. That is the remote-DNS behaviour that a socks5h:// proxy URL selects in curl and Python requests, and it also means the client’s dns() setting does not apply to SOCKS routes. proxymint Lab #1 measured on September 28, 2026 that requests carrying the hostname (CONNECT and socks5h) resolve on the proxy side of proxymint’s gateway. Whether SOCKS5 or HTTP is the better fit for a job is covered in SOCKS5 vs HTTP proxy.

These SOCKS details hold on the JVM. Android ships its own java.net implementation, so test a SOCKS login on a device before relying on it there; the HTTP proxy type behaves the same on both.

Kotlin, Android and the system proxy

A client with no proxy() and no proxySelector() takes ProxySelector.getDefault() when it is built and asks that selector on every new route, so a ProxySelector.setDefault() after the client exists does not reach it. On a server JVM the default selector reads http.proxyHost, https.proxyHost, socksProxyHost and http.nonProxyHosts, so a -Dhttps.proxyHost flag in JAVA_TOOL_OPTIONS sends OkHttp traffic through that proxy without a line of OkHttp code. OkHttp does not read HTTP_PROXY or HTTPS_PROXY environment variables; nothing in its source looks them up.

On Android, the ProxyInfo reference says the device’s proxy configuration is “already integrated within the java.net and Apache HTTP stack”. OkHttp asks that same java.net selector, so a proxy set in Wi-Fi settings, or by a debugging tool, can reach an app’s OkHttp traffic. Clients that must never use a device proxy set proxy(Proxy.NO_PROXY); one that must always use a specific gateway sets proxy() explicitly, which wins over the selector.

OkHttp 5.5.0 also changed one failure mode: when the system selector throws, OkHttp now falls back to no proxy instead of crashing the call (changelog, August 16, 2026).

OkHttp proxy rotation and reuse

An OkHttp proxy setup and a rotating gateway pull in opposite directions: the gateway picks a new exit for new traffic, and OkHttp keeps connections open for reuse. The default pool keeps up to 5 idle connections for 5 minutes, and an HTTPS connection through a proxy is a CONNECT tunnel to one exit. After the CONNECT the gateway sees only encrypted bytes, so it cannot switch exits inside a tunnel. One client sending 20 calls to the same https:// host reuses one tunnel and shows one IP.

This test makes that visible. It runs 20 calls on the default pooled client and 20 on a copy whose pool keeps no idle connections, so every call opens a new tunnel:

import java.net.InetSocketAddress
import java.net.Proxy
import java.util.concurrent.TimeUnit
import okhttp3.ConnectionPool
import okhttp3.Credentials
import okhttp3.OkHttpClient
import okhttp3.Request

fun distinctExits(client: OkHttpClient, calls: Int): Int {
    val exits = mutableSetOf<String>()
    repeat(calls) {
        val request = Request.Builder().url(System.getenv("ECHO_URL")).build()
        client.newCall(request).execute().use { exits += it.body.string().trim() }
    }
    return exits.size
}

fun main() {
    val host = System.getenv("PROXY_HOST")
    val port = System.getenv("PROXY_PORT").toInt()
    val credential = Credentials.basic(System.getenv("PROXY_USER"), System.getenv("PROXY_PASS"))

    val pooled = OkHttpClient.Builder()
        .proxy(Proxy(Proxy.Type.HTTP, InetSocketAddress(host, port)))
        .proxyAuthenticator { _, response ->
            if (response.request.header("Proxy-Authorization") != null) null
            else response.request.newBuilder().header("Proxy-Authorization", credential).build()
        }
        .build()

    // Same settings, but no idle connection survives: every call opens a new tunnel.
    val fresh = pooled.newBuilder()
        .connectionPool(ConnectionPool(0, 1, TimeUnit.SECONDS))
        .build()

    println("pooled client: ${distinctExits(pooled, 20)} distinct exits in 20 calls")
    println("no-reuse client: ${distinctExits(fresh, 20)} distinct exits in 20 calls")
}

Set ECHO_URL to any https:// IP echo endpoint. Against a local proxy that opens a new upstream connection per tunnel, the pooled client used 1 tunnel and the no-reuse client 20, on both OkHttp versions. Through a real rotating gateway, the no-reuse count shows how the gateway rotates; the pooled count shows how much of that the client was hiding. Each new tunnel costs a TCP connect, a CONNECT and a TLS handshake, so use the no-reuse client only for calls that need a fresh exit.

Rotation is set on the order, not in code. On proxymint’s rotating tiers, the order picks a new IP on every request, a timer from 5 to 60 minutes, or sticky for as long as the device stays online, and it can be changed later without new credentials. In code, OkHttp only decides whether connections are reused. What each session model breaks is covered in rotating vs sticky proxies.

A different proxy per request

OkHttpClient.newBuilder() returns a builder that shares the original client’s connection pool and dispatcher, so one derived client per proxy is cheap. Pooled connections are keyed by address, which includes the proxy, so clients with different proxies never share a tunnel. Here is a round robin over a list:

import java.net.InetSocketAddress
import java.net.Proxy
import java.util.concurrent.atomic.AtomicInteger
import okhttp3.Credentials
import okhttp3.OkHttpClient
import okhttp3.Request

class ProxyPool(base: OkHttpClient, endpoints: List<String>, user: String, pass: String) {
    private val credential = Credentials.basic(user, pass)
    private val next = AtomicInteger()

    // newBuilder() shares the base client's connection pool, dispatcher and threads.
    private val clients = endpoints.map { endpoint ->
        val (host, port) = endpoint.split(":")
        base.newBuilder()
            .proxy(Proxy(Proxy.Type.HTTP, InetSocketAddress(host, port.toInt())))
            .proxyAuthenticator { _, response ->
                if (response.request.header("Proxy-Authorization") != null) null
                else response.request.newBuilder().header("Proxy-Authorization", credential).build()
            }
            .build()
    }

    fun nextClient(): OkHttpClient = clients[Math.floorMod(next.getAndIncrement(), clients.size)]
}

fun main() {
    val pool = ProxyPool(
        base = OkHttpClient(),
        endpoints = System.getenv("PROXY_LIST").split(","), // "HOST1:PORT1,HOST2:PORT2"
        user = System.getenv("PROXY_USER"),
        pass = System.getenv("PROXY_PASS"),
    )
    repeat(4) {
        val request = Request.Builder().url("https://api.ipify.org").build()
        pool.nextClient().newCall(request).execute().use { println(it.body.string()) }
    }
}

Build the clients once and reuse them. Each derived client keeps its own warm tunnels, so calls alternate between proxies without paying a new handshake each time.

ProxySelector failover, not rotation

A ProxySelector that returns several proxies gives failover in a fixed order. OkHttp tries them in order and moves to the next one only when a connection fails; the builder documentation says they are “tried in sequence until a successful connection is established”. So the first healthy proxy takes every new connection, which makes a selector the right tool for a backup gateway. One OkHttp 5 detail: fast fallback, on by default, starts the next proxy if the current one has not finished its TCP connect within 250 ms, and the first to connect wins. A first proxy that is far away can therefore lose some connections to the second; fastFallback(false) restores strict order.

import java.io.IOException
import java.net.InetSocketAddress
import java.net.Proxy
import java.net.ProxySelector
import java.net.SocketAddress
import java.net.URI
import java.time.Duration
import okhttp3.Credentials
import okhttp3.OkHttpClient
import okhttp3.Request

// Tries each proxy in order on a connection failure. Not round robin: the first
// healthy proxy takes every call.
class FailoverSelector(private val proxies: List<Proxy>) : ProxySelector() {
    override fun select(uri: URI): List<Proxy> = proxies
    override fun connectFailed(uri: URI, sa: SocketAddress, ioe: IOException) {
        System.err.println("proxy $sa failed: ${ioe.message}")
    }
}

fun main() {
    val proxies = System.getenv("PROXY_LIST").split(",").map {
        val (host, port) = it.split(":")
        Proxy(Proxy.Type.HTTP, InetSocketAddress(host, port.toInt()))
    }
    val credential = Credentials.basic(System.getenv("PROXY_USER"), System.getenv("PROXY_PASS"))

    val client = OkHttpClient.Builder()
        .proxySelector(FailoverSelector(proxies))
        .proxyAuthenticator { _, response ->
            if (response.request.header("Proxy-Authorization") != null) null
            else response.request.newBuilder().header("Proxy-Authorization", credential).build()
        }
        .connectTimeout(Duration.ofSeconds(5))
        .build()

    val request = Request.Builder().url("https://api.ipify.org").build()
    try {
        client.newCall(request).execute().use { println("${it.code} ${it.body.string()}") }
    } catch (e: IOException) {
        println("all proxies failed: ${e.javaClass.simpleName}: ${e.message}")
    }
}

With a dead first proxy, the call succeeded through the second on both versions. OkHttp 5.5.0 also called connectFailed for the dead one; 4.12.0 did not, because reporting a failed TCP connect to the selector arrived in 5.0.0-alpha.15. With both dead, the call threw the first failure, ConnectException: Failed to connect to /HOST:PORT. How long a silent proxy holds the call up depends on the version. When the first proxy dropped packets under a 5 second connectTimeout, 4.12.0 waited the full timeout before trying the second, while 5.5.0 started the second after 250 ms. On 4.x, or with fast fallback off, keep connectTimeout short.

When the OkHttp proxy fails

Each failure has its own exception, and the message says which layer broke:

Exception and messageWhat happenedFix
IOException: Failed to authenticate with proxyCONNECT got 407 and the authenticator returned nullCredentials on proxyAuthenticator, check username and password
ProtocolException: Too many tunnel connections attempted: 21Unguarded authenticator, proxy closes after each 407Add the guard, fix the credentials
IOException: Unexpected response code for CONNECT: 403 (or 502)Proxy refused the tunnel for a reason other than authCheck the target, the port and the account’s status with the provider
ProtocolException: Too many follow-up requests: 21Unguarded retries or a redirect loop on an http:// URLAdd the guard, use https://
SocketException: SOCKS : authentication failedSOCKS5 login refused (JDK 21 wording)Default java.net.Authenticator, scoped to SOCKS5
ConnectException: Failed to connect to /HOST:PORTNothing listening at the proxy addressHost, port, firewall
SocketTimeoutExceptionProxy unreachable within connectTimeout, or a slow answer within readTimeoutShorter connectTimeout plus failover
UnknownHostExceptionThe proxy hostname does not resolve on the clientDNS on the client machine

Rows one, two, five and six were reproduced against a local test proxy on OkHttp 4.12.0 and 5.5.0; the others are the messages in the OkHttp source.

Verify the exit, pick the type

Check an OkHttp proxy with one call to an https:// IP echo endpoint through the client and one without it; the two answers must differ. Use an https:// URL for this. On proxymint’s per-GB gateway, plain http:// requests get a 301 to the https:// URL (Lab #1, September 28, 2026), and OkHttp follows redirects by default, so an http:// check works but costs an extra round trip and hides the redirect.

Then match the proxy category to the target. The OkHttp proxy code is identical for all of them:

  • Consumer-facing sites that score IP reputation, such as retail catalogs and search results by location: rotating residential proxies, with the no-reuse client where every call needs a fresh exit.
  • APIs and lenient targets at volume: datacenter proxies, where one long-lived pooled client per IP is the efficient setup.
  • One stable identity for days, such as a partner API that only accepts one fixed address: static ISP proxies, with no rotation to manage in code.

Every proxymint tier speaks HTTP and SOCKS5 with username and password authentication, so the proxyAuthenticator and SOCKS examples above work on each of them with the host, port and login from the order.

Frequently asked questions

Create a java.net.Proxy, for example Proxy(Proxy.Type.HTTP, InetSocketAddress(HOST, PORT)), and pass it to OkHttpClient.Builder.proxy(). Build the client once and reuse it. Without an explicit proxy, OkHttp uses the builder's proxySelector, or the JVM's default ProxySelector, which follows system properties and the device proxy on Android.

Set proxyAuthenticator on the builder and return response.request.newBuilder().header("Proxy-Authorization", Credentials.basic(USERNAME, PASSWORD)).build(), but return null if the request already carries that header. OkHttp calls the proxy authenticator before every CONNECT, so the credentials go out on the first tunnel request. Do not use authenticator(), which answers the website's 401 challenges.

The proxy answered the CONNECT request with 407 and the proxy authenticator returned null. Usually no proxyAuthenticator is set because the header was added in an interceptor, which never reaches the CONNECT, or the proxy refused the username and password that were sent. Check that the credentials are on proxyAuthenticator and that they are correct.

Yes, with Proxy.Type.SOCKS, but the login comes from java.net.Authenticator.setDefault, not from proxyAuthenticator, because OkHttp hands the SOCKS handshake to the JDK. Answer only when the requesting protocol is SOCKS5 and the host and port match the proxy. OkHttp sends the target hostname to the SOCKS proxy, so DNS resolves on the proxy side.

Yes, when the client has no explicit proxy and no proxySelector. It then uses ProxySelector.getDefault(), taken when the client is built, which reads the http.proxyHost, https.proxyHost and socksProxyHost system properties on the JVM and reflects the device proxy on Android. OkHttp does not read HTTP_PROXY environment variables. Set proxy(Proxy.NO_PROXY) to ignore any system proxy.

With one rotating gateway, the rotation mode is set on the proxy order, and OkHttp only decides whether connections are reused: a pooled client keeps one tunnel and one exit, while a client with ConnectionPool(0, 1, TimeUnit.SECONDS) opens a new tunnel per call. With a list of proxies, build one client per proxy with newBuilder() and pick them round robin.

Call client.newBuilder().proxy(otherProxy).build() and send that request through the derived client. Derived clients share the original connection pool and dispatcher, and connections are never shared across different proxies, so this is cheap. Build the derived clients once and reuse them rather than creating one per request.

Researched and drafted with AI tools, then checked against our publishing rules before it went live: 11 outside links, 8 links to other proxymint pages, no prices and a fact-check pass against our product records. How we write.