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: timeout after 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() or response.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. 1.Always close response bodies with use:
  2. 2.```kotlin
  3. 3.// WRONG: body not closed on error
  4. 4.val response = client.newCall(request).execute()
  5. 5.if (response.isSuccessful) {
  6. 6.val data = response.body?.string()
  7. 7.// If unsuccessful, body is never closed
  8. 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. 1.Use proper cleanup in try-finally:
  2. 2.```kotlin
  3. 3.fun fetchData(url: String): String? {
  4. 4.val request = Request.Builder().url(url).build()
  5. 5.var response: Response? = null
  6. 6.try {
  7. 7.response = client.newCall(request).execute()
  8. 8.return response.body?.string()
  9. 9.} finally {
  10. 10.response?.close() // Always closes the body and releases connection
  11. 11.}
  12. 12.}
  13. 13.`
  14. 14.Configure connection pool limits:
  15. 15.```kotlin
  16. 16.val client = OkHttpClient.Builder()
  17. 17..connectionPool(ConnectionPool(
  18. 18.maxIdleConnections = 5,
  19. 19.keepAliveDuration = 5,
  20. 20.timeUnit = TimeUnit.MINUTES
  21. 21.))
  22. 22..connectTimeout(10, TimeUnit.SECONDS)
  23. 23..readTimeout(10, TimeUnit.SECONDS)
  24. 24..writeTimeout(10, TimeUnit.SECONDS)
  25. 25..build()
  26. 26.`
  27. 27.Add an interceptor to detect leaked responses:
  28. 28.```kotlin
  29. 29.class LeakDetectionInterceptor : Interceptor {
  30. 30.override fun intercept(chain: Interceptor.Chain): Response {
  31. 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. 1.Handle streaming responses correctly:
  2. 2.```kotlin
  3. 3.fun downloadFile(url: String, destination: File) {
  4. 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.body without 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