Fix SocketTimeoutException When Calling API in Android (OkHttp / Retrofit)

intermediate๐Ÿ“ฑ Android2026-06-03| Android 8.0+ / Kotlin or Java / OkHttp 4.x / Retrofit 2.x

Error Message

java.net.SocketTimeoutException: failed to connect to api.example.com/192.168.1.1 (port 443) from /192.168.1.2 (port 52341) after 15000ms
#network#http#timeout#socket#okhttp#retrofit

TL;DR

The TCP connection timed out before the server responded. Three things fix this in most cases: raise your OkHttp timeouts to something realistic for mobile networks, verify the endpoint is actually reachable from the device's network, and add retry logic for transient failures.

The Error

java.net.SocketTimeoutException: failed to connect to api.example.com/192.168.1.1 (port 443) from /192.168.1.2 (port 52341) after 15000ms

This shows up in logcat when Android tries to open a TCP connection to your API server but gets no response within the timeout window โ€” 15 seconds in this case. The connection never completed. That makes this a connect timeout, not a read timeout. The distinction matters when you start debugging.

Root Cause

Several things can trigger this โ€” some obvious, some not:

  • Server unreachable or firewall blocking port 443 โ€” the SYN packet goes out but the server never replies with SYN-ACK
  • DNS resolved to wrong or stale IP โ€” the IP in the error message doesn't match what you expect
  • Device on restricted network โ€” corporate WiFi, VPN, or a hotspot blocking outbound connections
  • Timeout configured too low โ€” OkHttp defaults to 10s connect/read/write, which regularly fails on slow 3G or congested networks
  • Server under load โ€” accepting connections but the queue is full and packets get dropped

Fix 1: Tune Timeouts in OkHttp / Retrofit

Most Retrofit setups use OkHttp under the hood. The defaults โ€” 10s connect, 10s read, 10s write โ€” hold up fine on fast connections but fail regularly on mobile. Bump them:

val okHttpClient = OkHttpClient.Builder()
    .connectTimeout(30, TimeUnit.SECONDS)   // time to establish TCP connection
    .readTimeout(30, TimeUnit.SECONDS)      // time waiting for server to respond
    .writeTimeout(30, TimeUnit.SECONDS)     // time to send the request body
    .build()

val retrofit = Retrofit.Builder()
    .baseUrl("https://api.example.com/")
    .client(okHttpClient)
    .addConverterFactory(GsonConverterFactory.create())
    .build()

30s is a sensible starting point for most mobile APIs. Don't go above 60s โ€” at that point you're just making users stare at a spinner on a connection that's clearly not coming back.

Fix 2: Add Retry Logic for Transient Failures

OkHttp has a built-in retry mechanism, but it's conservative. For temporary network blips or DNS hiccups, a custom retry interceptor with backoff is more reliable:

class RetryInterceptor(private val maxRetries: Int = 3) : Interceptor {
    override fun intercept(chain: Interceptor.Chain): Response {
        var attempt = 0
        var lastException: IOException? = null

        while (attempt < maxRetries) {
            try {
                return chain.proceed(chain.request())
            } catch (e: SocketTimeoutException) {
                lastException = e
                attempt++
                if (attempt < maxRetries) {
                    Thread.sleep(1000L * attempt) // backoff: 1s, 2s, 3s
                }
            }
        }
        throw lastException ?: IOException("Unknown error after $maxRetries retries")
    }
}

// Attach to OkHttp
val okHttpClient = OkHttpClient.Builder()
    .addInterceptor(RetryInterceptor(maxRetries = 3))
    .connectTimeout(30, TimeUnit.SECONDS)
    .build()

Fix 3: Fail Fast With a Connectivity Check

Waiting 30 seconds for a timeout is awful UX. Check network availability before making the call โ€” fail immediately with a useful message instead:

fun isNetworkAvailable(context: Context): Boolean {
    val cm = context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
    val network = cm.activeNetwork ?: return false
    val capabilities = cm.getNetworkCapabilities(network) ?: return false
    return capabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
}

// In your Repository
suspend fun fetchData(): Result<MyData> {
    if (!isNetworkAvailable(context)) {
        return Result.failure(IOException("No internet connection"))
    }
    return try {
        Result.success(api.getData())
    } catch (e: SocketTimeoutException) {
        Result.failure(e)
    }
}

Fix 4: Debug the DNS Resolution

The IP address in the error is your first clue. api.example.com/192.168.1.1 โ€” that's a private IP. If you're calling a public API and seeing a 192.168.x.x or 10.x.x.x address, DNS is resolving to the wrong host. Stale cache, split-horizon DNS, or a misconfigured VPN are the usual suspects.

# Check what IP the device resolves
adb shell nslookup api.example.com

# Verify basic connectivity
adb shell ping -c 3 api.example.com

# Check if port 443 is reachable
adb shell nc -zv api.example.com 443

If the IP looks wrong, switch from WiFi to mobile data and retry. Call succeeds on mobile data? The WiFi network is filtering or misrouting your traffic โ€” not your code.

Fix 5: Check Network Security Config

Android 9+ blocks cleartext HTTP by default. If your app accidentally hits an HTTP endpoint โ€” wrong base URL, or a redirect that drops back to HTTP โ€” Android kills the connection and surfaces it as a network error. Check your config:

<!-- res/xml/network_security_config.xml -->
<network-security-config>
    <domain-config cleartextTrafficPermitted="false">
        <domain includeSubdomains="true">api.example.com</domain>
    </domain-config>
</network-security-config>

Verification Steps

Once you've applied a fix, here's how to confirm it actually worked:

  • Test on a real device, not the emulator โ€” emulator networking behaves differently
  • Filter logcat by OkHttp tag โ€” you should see request/response logs, not exception traces
  • Test on both WiFi and mobile data to catch network-specific issues
  • Add OkHttp's logging interceptor in debug builds to see full timing:
// Debug builds only
if (BuildConfig.DEBUG) {
    val logging = HttpLoggingInterceptor().apply {
        level = HttpLoggingInterceptor.Level.HEADERS
    }
    builder.addInterceptor(logging)
}

Tips

If the IP in the error looks like a private address (10.x.x.x, 172.16โ€“31.x.x, 192.168.x.x) but you're hitting a public API, DNS is sending you somewhere wrong. The IP Subnet Calculator on ToolCraft lets you quickly check whether an IP is private or publicly routable โ€” runs entirely in the browser, nothing uploaded. Useful when you're debugging in the field without your usual tools.

Further Reading

Related Error Notes