Introduction
OkHttp maintains a connection pool to reuse HTTP connections efficiently. However, each response body holds a reference to a connection. If response bodies are not closed, connections are never returned to the pool. Eventually, the pool is exhausted, new requests block waiting for a connection, and the app appears to hang.
Symptoms
- HTTP requests hang indefinitely with no response
java.net.SocketTimeoutException: timeoutafter connection pool is full- Logcat shows
A connection to ... was leaked. Did you forget to close a response body? - Works for a few requests then all subsequent requests time out
- Thread dump shows threads blocked in
ConnectionPool.get()
Example warning:
``
WARNING: A connection to https://api.example.com was leaked.
Did you forget to close a response body?
at okhttp3.ConnectionPool$1.run(ConnectionPool.java:67)
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1167)
Common Causes
- Reading response code without consuming the body
- Not calling
response.close()orresponse.body?.close()in finally block - Interceptor that reads body but does not pass it through
- Error responses where the body is ignored
- Using
response.body?.string()but not closing the response
Step-by-Step Fix
- 1.Always close response bodies with use:
- 2.```kotlin
- 3.// WRONG: body not closed on error
- 4.val response = client.newCall(request).execute()
- 5.if (response.isSuccessful) {
- 6.val data = response.body?.string()
- 7.// If unsuccessful, body is never closed
- 8.}
// CORRECT: use auto-close val response = client.newCall(request).execute() response.use { if (response.isSuccessful) { val data = response.body?.string() processData(data) } else { Log.e(TAG, "HTTP ${response.code}: ${response.message}") } } // Automatically closes the response body ```
- 1.Use proper cleanup in try-finally:
- 2.```kotlin
- 3.fun fetchData(url: String): String? {
- 4.val request = Request.Builder().url(url).build()
- 5.var response: Response? = null
- 6.try {
- 7.response = client.newCall(request).execute()
- 8.return response.body?.string()
- 9.} finally {
- 10.response?.close() // Always closes the body and releases connection
- 11.}
- 12.}
- 13.
` - 14.Configure connection pool limits:
- 15.```kotlin
- 16.val client = OkHttpClient.Builder()
- 17..connectionPool(ConnectionPool(
- 18.maxIdleConnections = 5,
- 19.keepAliveDuration = 5,
- 20.timeUnit = TimeUnit.MINUTES
- 21.))
- 22..connectTimeout(10, TimeUnit.SECONDS)
- 23..readTimeout(10, TimeUnit.SECONDS)
- 24..writeTimeout(10, TimeUnit.SECONDS)
- 25..build()
- 26.
` - 27.Add an interceptor to detect leaked responses:
- 28.```kotlin
- 29.class LeakDetectionInterceptor : Interceptor {
- 30.override fun intercept(chain: Interceptor.Chain): Response {
- 31.val response = chain.proceed(chain.request())
// Track open responses val tracker = response.newBuilder() .tag(StackTraceElement::class.java, Throwable("Response created here")) .build()
return tracker } }
// Add to client val client = OkHttpClient.Builder() .addInterceptor(LeakDetectionInterceptor()) .build() ```
- 1.Handle streaming responses correctly:
- 2.```kotlin
- 3.fun downloadFile(url: String, destination: File) {
- 4.val request = Request.Builder().url(url).build()
client.newCall(request).enqueue(object : Callback { override fun onResponse(call: Call, response: Response) { response.use { if (!response.isSuccessful) return
val body = response.body ?: return destination.outputStream().use { output -> body.byteStream().use { input -> input.copyTo(output) } } } }
override fun onFailure(call: Call, e: IOException) { Log.e(TAG, "Download failed", e) } }) } ```
Prevention
- Always use
response.use { }(Kotlin's auto-close) for OkHttp responses - Never read
response.bodywithout closing the response - Set reasonable timeouts to prevent indefinite blocking
- Monitor connection pool usage in production
- Add leak detection in debug builds
- Use
response.peekBody()in interceptors instead of consuming the body