androidengineers.Book a session

Caching Strategies

HTTP Caching: ETag, Max-Age, Revalidate

article20 minMedium

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:

  1. Stores responses that include Cache-Control, Expires, or Last-Modified headers
  2. Returns cached responses when max-age hasn't expired
  3. Sends conditional requests (If-None-Match, If-Modified-Since) when expired
  4. Accepts 304 Not Modified and 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

DirectiveMeaning
max-age=NCache valid for N seconds
no-cacheMust revalidate before serving from cache
no-storeDon't cache at all (sensitive data)
must-revalidateDon't serve stale, even when offline
publicCan be stored in shared caches
privateOnly for the individual user

Key Takeaways

ConceptRule
OkHttp CacheSingle setup; OkHttp honors server headers automatically
ETag / 304Saves bandwidth; server still handles the round-trip
FORCE_NETWORKUse for pull-to-refresh to bypass cache
maxStaleEnables offline reads from expired cache
no-storeRequired for auth tokens, payment data — never cache these

YOUR LEARNING JOURNEY

0 of 177 available lessons completed

Progress saved in this browser. No account needed.
HTTP Caching: ETag, Max-Age, Revalidate | Android System Design | Android Engineers