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: timeoutafter 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 responseresponse.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.**Always close response body using use block":
- 2.```kotlin
- 3.// WRONG - response body not closed on error path
- 4.fun fetchBad(): String? {
- 5.val response = client.newCall(request).execute()
- 6.if (!response.isSuccessful) return null // LEAK! body not closed
- 7.return response.body?.string()
- 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.**Handle partial response body reading":
- 2.```kotlin
- 3.// When you only need part of the response, still close the body
- 4.fun fetchPartial(client: OkHttpClient, url: String): String {
- 5.val request = Request.Builder().url(url).build()
- 6.client.newCall(request).execute().use { response ->
- 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.**Tune connection pool for high concurrency":
- 2.```kotlin
- 3.import okhttp3.ConnectionPool
- 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.**Detect leaked connections in development":
- 2.```kotlin
- 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