HTTP caching lets clients avoid redundant network requests using standard headers. OkHttp implements this automatically — but only when you configure it correctly.
The Two Caching Models
Freshness-based: The server declares how long a response is valid. The client reuses it without contacting the server during that window.
Cache-Control: max-age=3600, public
Validation-based: The server issues a fingerprint (ETag). When the client revalidates, it sends the ETag; the server responds with 304 Not Modified if nothing changed — saving response body bandwidth.
ETag: "abc123"
Last-Modified: Mon, 28 Jul 2025 10:00:00 GMT
OkHttp Cache Setup
val cache = Cache(
directory = File(context.cacheDir, "http_cache"),
maxSize = 10L * 1024 * 1024 // 10 MB
)
val okHttpClient = OkHttpClient.Builder()
.cache(cache)
.build()
With this in place, OkHttp:
- Stores responses that include
Cache-Control,Expires, orLast-Modifiedheaders - Returns cached responses when
max-agehasn't expired - Sends conditional requests (
If-None-Match,If-Modified-Since) when expired - Accepts
304 Not Modifiedand returns the cached body
Request-Side Cache Control
Force network (ignore cache):
val request = Request.Builder()
.url("https://api.example.com/articles")
.cacheControl(CacheControl.FORCE_NETWORK)
.build()
Force cache (fail if not cached):
val request = Request.Builder()
.url("https://api.example.com/articles")
.cacheControl(CacheControl.FORCE_CACHE)
.build()
Custom stale-while-revalidate:
val cacheControl = CacheControl.Builder()
.maxAge(1, TimeUnit.HOURS)
.maxStale(1, TimeUnit.DAYS) // accept up to 1 day stale if network unavailable
.build()
Offline-First Strategy
Use maxStale to serve cached content when offline:
class CachingInterceptor(private val context: Context) : Interceptor {
override fun intercept(chain: Interceptor.Chain): Response {
var request = chain.request()
if (!context.isNetworkAvailable()) {
// No network: accept stale cache up to 7 days
request = request.newBuilder()
.cacheControl(CacheControl.Builder().maxStale(7, TimeUnit.DAYS).build())
.build()
}
return chain.proceed(request)
}
}
fun Context.isNetworkAvailable(): Boolean {
val cm = getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
return cm.activeNetwork != null
}
Reading Cache Stats
// After making a request:
println("Cache hit: ${response.cacheResponse != null}")
println("Network used: ${response.networkResponse != null}")
println("Cache hits: ${cache.hitCount()}")
println("Network requests: ${cache.networkCount()}")
println("Cache size: ${cache.size()} bytes")
Common Cache-Control Directives
| Directive | Meaning |
|---|---|
max-age=N | Cache valid for N seconds |
no-cache | Must revalidate before serving from cache |
no-store | Don't cache at all (sensitive data) |
must-revalidate | Don't serve stale, even when offline |
public | Can be stored in shared caches |
private | Only for the individual user |
Key Takeaways
| Concept | Rule |
|---|---|
OkHttp Cache | Single setup; OkHttp honors server headers automatically |
| ETag / 304 | Saves bandwidth; server still handles the round-trip |
FORCE_NETWORK | Use for pull-to-refresh to bypass cache |
maxStale | Enables offline reads from expired cache |
no-store | Required for auth tokens, payment data — never cache these |