Cache-Control is an HTTP header that defines caching rules for resources on the client side, proxy servers, and CDNs using a set of directives. Unlike the deprecated Expires header, Cache-Control supports dozens of combinations: max-age sets the lifetime in seconds, private and public control cache availability, no-cache and no-store — forced verification. According to Google Web Dev (2025), proper Cache-Control configuration can reduce page load times by 50-80% for repeat visits. This makes the header critically important for web and mobile application performance.
Key Takeaways
Cache-Control is an HTTP header, standardized in HTTP/1.1 (RFC 7234), that allows the server to specify how and for how long clients, proxies, and CDNs can cache the response. Unlike Expires (HTTP/1.0), Cache-Control uses directives — text commands combined with commas: Cache-Control: public, max-age=3600, must-revalidate. The header provides fine-grained control over each link in the caching chain.
Caching is one of the fundamental mechanisms of web and mobile application performance. Without it, every user request would go directly to the server, causing excessive load and latency. Cache-Control defines three caching levels: browser/application (private cache), proxy servers (shared cache), and CDN (distributed cache). Each level interprets directives differently.
Incorrect Cache-Control configuration is one of the most common causes of performance issues. Too aggressive caching leads to users seeing outdated data. Too weak caching leads to excessive server requests and slow loading. According to Akamai (2025), optimizing Cache-Control for static content reduces server load by 70-90% and improves load time by 40-60% for mobile users.
Cache-Control appeared in HTTP/1.1 (RFC 2616, 1999) as a replacement for Expires. Expires had a fundamental problem: it used an absolute date that depended on the server and client time zones. Cache-Control solved this problem by switching to relative time (max-age in seconds from the moment the response is received). Later, in RFC 7234 (2014), new directives were added: immutable for static assets, stale-while-revalidate and stale-if-error for deferred validation.
Cache-Control includes more than 10 directives divided into three groups: request directives (client → server), response directives (server → client), and extensions. In practice, mobile development uses 6-7 main response directives that cover 95% of caching scenarios. Let’s look at each one with examples and recommendations.
| Directive | Meaning | Example |
|---|---|---|
| max-age | Lifetime in seconds from the moment of the response | max-age=3600 — 1 hour |
| s-maxage | max-age for shared cache (proxy, CDN) | s-maxage=86400 — 1 day for CDN |
| public | Allows caching by everyone (including proxies) | public, max-age=3600 |
| private | Allows cache only for the browser/application | private, max-age=600 |
| no-cache | Do not use without validation (304 required) | no-cache |
| no-store | Completely prohibit caching | no-store |
| must-revalidate | After max-age, must revalidate with the origin | max-age=3600, must-revalidate |
| immutable | The resource will not change (for versioned static assets) | max-age=31536000, immutable |
max-age is the most important directive. It prohibits the client from making a request to the server for the specified time. For static assets (CSS, JS, images), max-age is usually set from 1 day to 1 year. For API responses — from 0 seconds (data always fresh) to 5-10 minutes (reference data). s-maxage allows setting different lifetimes for CDN and browser: the CDN stores a copy for 1 day, the browser for 1 hour.
These two directives are often confused. no-cache does not prohibit caching — it requires validating the cached copy on each use via a conditional request (If-Modified-Since or If-None-Match). If the server responds with 304 — the client uses the cache. If 200 — it updates. no-store, on the other hand, completely prohibits saving the response in any cache, including disk and memory. Use no-store only for sensitive data — tokens, payment data, personal documents.
The Expires header (HTTP/1.0) also specifies the resource’s lifetime but uses an absolute date: Expires: Thu, 03 Jul 2026 12:00:00 GMT. Cache-Control max-age uses relative time from the moment of the response. The difference is critical for distributed systems: if the server and client are in different time zones, Expires can be interpreted incorrectly. Cache-Control does not have this problem — 3600 seconds is always 3600 seconds.
When both headers are present, Cache-Control takes priority over Expires. This is defined in RFC 7234: “If a response includes a Cache-Control field with the max-age directive, a recipient MUST ignore the Expires field.” In practice, it is recommended not to return Expires at all for modern clients, since Cache-Control covers all Expires scenarios. However, for backward compatibility with old proxies and browsers, both headers can be returned.
Expires has survived mainly for static content on Nginx and Apache — these servers automatically add both headers. If your project encounters Expires without Cache-Control, replace it with Cache-Control with max-age: caching control accuracy improves, and dependence on the time zone is eliminated. For migration, it is enough to configure the server to add Cache-Control instead of Expires.
# Nginx: Cache-Control for static files
location ~* \.(jpg|jpeg|png|gif|ico|css|js)$ {
expires 30d;
add_header Cache-Control "public, immutable, max-age=2592000";
}
# Different policies for different content types
location /api/config {
expires -1;
add_header Cache-Control "no-cache, must-revalidate";
}
location /api/static-data {
expires 5m;
add_header Cache-Control "public, max-age=300";
}
In the Nginx configuration, static files (CSS, JS, images) are set with Cache-Control for 30 days with the immutable attribute — this attribute tells the browser that the resource never changes at this URL (versioning via hash in the filename). API endpoints use no-cache for dynamic data and public with a short max-age for reference data — frequently requested and rarely changing lists.
In mobile applications, Cache-Control plays a special role due to the limitations of mobile networks: high latency, unstable connection, traffic limits. Proper caching allows displaying data to the user instantly, even offline, and updating it in the background. OkHttp on Android and URLSession on iOS have built-in caching systems that respect Cache-Control.
OkHttp uses CacheInterceptor, which reads Cache-Control from the response and automatically manages caching. If the server returned Cache-Control: max-age=3600, OkHttp will not make a request to the server for an hour. After max-age expires, OkHttp sends a conditional request with If-Modified-Since and If-None-Match. Cache configuration in OkHttp: OkHttpClient.Builder().cache(Cache(directory, maxSize)).
fun createCachedClient(cacheDir: File): OkHttpClient {
return OkHttpClient.Builder()
.cache(Cache(cacheDir, 10L * 1024 * 1024))
.addNetworkInterceptor { chain ->
val response = chain.proceed(chain.request())
response.newBuilder()
.header("Cache-Control",
"public, max-age=300")
.removeHeader("Pragma")
.build()
}
.build()
}
The code creates an OkHttpClient with a 10 MB cache and overrides Cache-Control via NetworkInterceptor. If the server does not return Cache-Control or uses Expires, the interceptor adds public, max-age=300 (5 minutes). The interceptor removes the deprecated Pragma header (HTTP/1.0) for compatibility. Caching on iOS works similarly through URLCache.shared with memoryCapacity and diskCapacity settings.
The stale-while-revalidate directive allows showing the user a stale cache while the application fetches fresh data in the background. This provides an instant response effect: the user sees content immediately, and after a second it updates to the current version. Supported by OkHttp starting from version 3.10 and URLCache on iOS 14+. Example: Cache-Control: max-age=3600, stale-while-revalidate=300 — 1 hour of fresh cache, then 5 minutes of showing stale data with background refresh.
Different resource types require different caching strategies. Let’s look at optimal configurations for typical scenarios in mobile development. For static content with a hash in the filename (bundle.abc123.js), you can set max-age up to 1 year with immutable. For API lists that are rarely updated (directories, categories) — max-age from 5 minutes to 1 hour with stale-while-revalidate.
| Resource Type | Cache-Control | Explanation |
|---|---|---|
| Versioned static assets | public, max-age=31536000, immutable | 1 year, files do not change (hash in URL) |
| Non-versioned static assets | public, max-age=86400, must-revalidate | 1 day with forced revalidation after |
| API: reference data | public, max-age=600, stale-while-revalidate=60 | 10 minutes cache + 1 minute stale |
| API: user data | private, max-age=60 | 1 minute, only for a specific user |
| API: sensitive data | no-store | Complete cache prohibition |
| HTML pages | no-cache, must-revalidate | Validation on each request, 304 if unchanged |
It is important to remember security: for responses containing personal user data, always set private. Without this directive, a public proxy (e.g., corporate) can cache the response and serve it to another user. For authentication tokens and payment information, use no-store — even a private cache should not store this data on disk.
To verify Cache-Control correctness, use the Age header (how many seconds the cache has been stored) and X-Cache (hit/miss on CDN). In the browser — the Network tab, the Size column shows “from disk cache” or “304 Not Modified”. If a resource should be cached but loads every time, check whether the server is adding Cache-Control: no-cache or Pragma: no-cache along with your directives.
Frequently Asked Questions
max-age applies to all caches (including browsers), s-maxage only applies to shared caches (proxies, CDNs). If s-maxage is specified, CDN ignores max-age and uses s-maxage. This allows setting different lifetimes for the browser and CDN.
No, after sending a response with max-age, the client will not make a request until the timer expires. For immediate cache invalidation, you need to change the resource URL (add a version/hash) and send push notifications or WebSocket messages for forced reset.
The immutable directive (RFC 8246) tells the browser that the resource will never change at this URL. The browser does not even attempt to make a conditional request when refreshing the page — it uses the cache until max-age expires. Works only with versioned files.
Googlebot takes Cache-Control into account: long caching speeds up repeat crawling. noindex with fast cache is fine. no-store can slow down indexing because Googlebot will load the page from scratch every time. Too short max-age increases server load during crawling.
Through helmet or middleware: res.set(‘Cache-Control’, ‘public, max-age=3600’). For static files, use express.static with the maxAge parameter: express.static(‘public’, {maxAge: ‘1y’}). For dynamic routes — individually in each handler.
Summary
We will develop a mobile application turnkey
IT Sectr creates iOS and Android applications for startups and businesses since 2017. We will advise you and propose the best solution.
Read also