Introduction

OkHttp reuses connections through its connection pool. Each response body holds a connection until it is fully consumed or explicitly closed. If a response body is not closed (or not fully read), the connection is never returned to the pool. After all connections are exhausted, new requests fail with java.net.SocketTimeoutException: timeout because no connections are available.

Symptoms

  • java.net.SocketTimeoutException: timeout after many requests
  • OkHttp logs show A connection to ... was leaked. Did you forget to close a response body?
  • All requests hang after a certain number of successful calls
  • Connection pool shows 0 available connections
  • Works for small number of requests but fails under load

OkHttp warning: `` WARNING: A connection to https://api.example.com was leaked. Did you forget to close a response body? To see where this was allocated, set the OkHttpClient logger level to FINE: Logger.getLogger(OkHttpClient.class.getName()).setLevel(Level.FINE);

Common Causes

  • response.body() not closed after reading only part of the response
  • response.body()?.string() called but exception thrown before close
  • Response body consumed in interceptor but not replaced
  • use {} block not used for response body
  • Connection pool max size too small for concurrent requests

Step-by-Step Fix

  1. 1.**Always close response body using use block":
  2. 2.```kotlin
  3. 3.// WRONG - response body not closed on error path
  4. 4.fun fetchBad(): String? {
  5. 5.val response = client.newCall(request).execute()
  6. 6.if (!response.isSuccessful) return null // LEAK! body not closed
  7. 7.return response.body?.string()
  8. 8.}

// CORRECT - use block ensures body is closed fun fetchGood(): String? { val response = client.newCall(request).execute() return response.use { // use {} auto-closes response if (!response.isSuccessful) return null response.body?.string() } }

// CORRECT - try-finally fun fetchSafe(): String? { val response = client.newCall(request).execute() try { if (!response.isSuccessful) return null return response.body?.string() } finally { response.close() // Always close, even on exception } } ```

  1. 1.**Handle partial response body reading":
  2. 2.```kotlin
  3. 3.// When you only need part of the response, still close the body
  4. 4.fun fetchPartial(client: OkHttpClient, url: String): String {
  5. 5.val request = Request.Builder().url(url).build()
  6. 6.client.newCall(request).execute().use { response ->
  7. 7.if (!response.isSuccessful) throw IOException("Unexpected code $response")

// Only read first 1KB val source = response.body?.source() val buffer = Buffer() source?.read(buffer, 1024)

// IMPORTANT: close the response even though we did not read all // use {} handles this return buffer.readUtf8() } } ```

  1. 1.**Tune connection pool for high concurrency":
  2. 2.```kotlin
  3. 3.import okhttp3.ConnectionPool
  4. 4.import java.util.concurrent.TimeUnit

val client = OkHttpClient.Builder() .connectionPool( ConnectionPool( maxIdleConnections = 100, // Default is 5 keepAliveDuration = 5, // Default is 5 keepAliveDurationUnit = TimeUnit.MINUTES ) ) .callTimeout(30, TimeUnit.SECONDS) .connectTimeout(10, TimeUnit.SECONDS) .readTimeout(30, TimeUnit.SECONDS) .build() ```

  1. 1.**Detect leaked connections in development":
  2. 2.```kotlin
  3. 3.import okhttp3.logging.HttpLoggingInterceptor

val loggingInterceptor = HttpLoggingInterceptor().apply { level = if (BuildConfig.DEBUG) { HttpLoggingInterceptor.Level.BODY } else { HttpLoggingInterceptor.Level.NONE } }

val client = OkHttpClient.Builder() .addInterceptor(loggingInterceptor) .eventListener(object : EventListener() { override fun callFailed(call: Call, ioe: IOException) { Log.e("OkHttp", "Call failed", ioe) }

override fun responseHeadersEnd(call: Call, response: Response) { // Track response creation Log.d("OkHttp", "Response received for ${call.request().url}") } }) .build() ```

Prevention

  • Always use response.use {} for automatic body closing
  • Never return from a function without closing the response body
  • Set reasonable connection pool size for your concurrency needs
  • Enable OkHttp logging in debug builds to detect leaks
  • Add leak detection to CI by checking for leaked connection warnings
  • Use Retrofit which handles response body closing automatically for successful responses