Okhttp internals
OkHttp Internals Deep Dive¶
Overview¶
OkHttp is the HTTP client layer that Retrofit delegates to. Understanding OkHttp's architecture is critical for production networking engineering on Android.
Core Concepts¶
Request/Response Lifecycle¶
- Application creates
Request - Interceptor chain processes
- Connection pool consulted
- TCP connection established or reused
- TLS handshake if HTTPS
- HTTP request sent
- Response received and parsed
- Interceptor chain (response phase)
- Response returned to caller
Interceptor Chain Architecture¶
OkHttp maintains two interceptor chains:
Application Interceptors: - See only your application's original request - Not called for retries or redirects - Perfect for logging, auth injection - Can short-circuit (return cached response)
Network Interceptors:
- See actual network traffic including redirects
- final response before actual network call
- Can measure real latency
- for debugging actual HTTP
Internal Implementation¶
Connection Pooling¶
OkHttp reuses connections via a pool:
OkHttpClient.Builder()
.connectionPool(ConnectionPool(
maxIdleConnections = 5,
keepAliveDuration = 5, TimeUnit.MINUTES
))
.build()
Pool keyed by: host:port:scheme
Benefits: - reduces TLS handshakes - improves throughput - reduces battery usage
Call Mechanics¶
Under the hood, synchronous Call:
interface Call <T> {
fun execute(): Response<T> // blocking
fun enqueue(callback: Callback<T>) // async
fun clone(): Call<T>
fun cancel()
}
Retrofit adapters convert this to suspend or Flow.
Request/Response Flow¶
Request Path¶
- Application calls
retrofit.userService.getUser(1) - Proxy method intercepts via dynamic code gen
- RequestBuilder creates HTTP request
- Retrofit passes to OkHttp Call
- Application interceptors run
- Connection pool consulted
- If new connection: DNS โ TCP โ TLS
- Request written to socket
- Network interceptors run
- Response read from socket
- Converter deserializes JSON to object
Response Path (Reverse)¶
Response flows back through interceptors in reverse (post-processing).
Code Examples¶
Custom Application Interceptor (Auth)¶
class AuthInterceptor : Interceptor {
override fun intercept(chain: Interceptor.Chain): Response {
val original = chain.request()
val token = getToken() // from storage
val authenticated = original.newBuilder()
.addHeader("Authorization", "Bearer $token")
.build()
return chain.proceed(authenticated)
}
}
val httpClient = OkHttpClient.Builder()
.addInterceptor(AuthInterceptor())
.build()
Retry Interceptor¶
class RetryInterceptor : Interceptor {
override fun intercept(chain: Interceptor.Chain): Response {
var request = chain.request()
var exception: IOException? = null
repeat(3) { attempt ->
try {
return chain.proceed(request)
} catch (e: IOException) {
exception = e
Thread.sleep(100L * (2.0).pow(attempt.toDouble()).toLong())
}
}
throw exception ?: IOException("retry failed")
}
}
Common Interview Questions¶
Q: Why does OkHttp cache connections? A: TLS handshakes are expensive (~100ms each). Reusing connections amortizes that cost.
Q: What's the difference between application and network interceptors? A: Application interceptors see redirects as one flow. Network interceptors see each redirect as separate call.
Q: Can you short-circuit OkHttp?
A: Yes, via application interceptor returning a cached response without calling chain.proceed().
Production Considerations¶
Timeouts Are Essential¶
OkHttpClient.Builder()
.connectTimeout(30, TimeUnit.SECONDS)
.readTimeout(30, TimeUnit.SECONDS)
.writeTimeout(30, TimeUnit.SECONDS)
.callTimeout(60, TimeUnit.SECONDS) // includes retries
.build()
Missing timeouts = potential ANRs on network hang.
Connection Pool Size¶
Default: 5 idle connections, 5 minute keep-alive.
For high-throughput apps, increase:
ConnectionPool(maxIdleConnections = 10, keepAliveDuration = 5, TimeUnit.MINUTES)
DNS Caching¶
OkHttp doesn't cache DNS by default. For production reliability:
class HostnameVerifier : HostnameVerifier {
private val systemVerifier = HttpsURLConnection.getDefaultHostnameVerifier()
override fun verify(hostname: String, session: SSLSession) =
systemVerifier.verify(hostname, session)
}
Performance Insights¶
Interceptor overhead¶
Each interceptor adds slight latency. Minimize number of interceptors.
Connection reuse > new connections¶
- New connection: ~150-300ms (TLS on 4G)
- Reused connection: ~5-10ms
Massive difference on recursive API calls.
Memory usage¶
Connection pools consume memory. Monitor: - active connections - idle connections - buffer sizes
Senior-Level Insights¶
Interceptor Composition¶
Multiple interceptors should be independent and composable:
// Anti-pattern: Logging depends on auth
interceptor1 = AuthInterceptor()
interceptor2 = LoggingInterceptor() // assumes auth token present
// Better: Logging logs whatever headers exist
Chunked Encoding¶
For streaming uploads, use RequestBody:
val body = object : RequestBody() {
override fun contentType() = MediaType.parse("text/plain")
override fun writeTo(sink: BufferedSink) {
// Stream directly, no buffer
for (chunk in dataStream) {
sink.write(chunk)
}
}
}
Connection Pool Lifecycle¶
OkHttp connection thread pool lives for entire app:
// Single shared instance (correct)
val httpClient = OkHttpClient.Builder()
.connectionPool(ConnectionPool())
.build()
// Within Retrofit
val retrofit = Retrofit.Builder()
.client(httpClient)
.build()
Creating new OkHttp clients = new thread pools = memory leak.