HTTP Caching Explained
A practical guide to HTTP caching: browser caches, shared caches, Cache-Control, ETag, Last-Modified, Expires, conditional requests, cache invalidation, CDNs and common caching mistakes.
HTTP caching is one of the most important mechanisms for improving web performance. Instead of downloading the same resource every time a user requests it, a browser, proxy, CDN or other cache can temporarily store the HTTP response and reuse it later.
Caching can significantly reduce latency, bandwidth usage and server load. It can also make a website feel much faster because cached resources may be served locally or from a nearby CDN edge. At the same time, incorrect caching can cause users to see stale content, outdated JavaScript bundles or responses that should never have been shared between users.
The key to HTTP caching is understanding what a response is allowed to be stored, how long it remains fresh, and what happens when the cache needs to verify whether the resource has changed.
What Is HTTP Caching?
HTTP caching is the process of storing HTTP responses so that future requests for the same resource can be served without contacting the origin server every time. The cache can exist in several places, including the user's browser, a corporate proxy, a reverse proxy or a CDN.
A cached response normally contains the response body together with metadata from the HTTP response headers. The cache uses that metadata to determine whether the stored response can be reused directly or whether it needs to contact the server to validate it.
For example, suppose a browser requests an image from a website. If the response allows caching for one day, the browser can keep the image locally. A later request for the same URL may be satisfied directly from the browser cache without downloading the image again.
| Component | Purpose |
|---|---|
| Browser cache | Stores resources locally on the user's device. |
| Shared cache | Stores responses that can potentially be reused by multiple users. |
| CDN cache | Stores resources at edge locations closer to users. |
| Reverse proxy cache | Caches responses in front of an origin server. |
Why HTTP Caching Matters
Without caching, every request may require a complete round trip to the origin server and a new response body. For popular resources, this creates unnecessary network traffic and server work.
Caching can improve several parts of the request lifecycle at once. A browser can avoid network requests entirely, while a CDN can serve a response from a nearby edge location instead of contacting the origin server located much farther away.
- Lower latency for repeat requests.
- Less bandwidth consumption.
- Fewer requests reaching the origin server.
- Lower infrastructure load.
- Better performance for static assets.
- Improved scalability for high-traffic resources.
- Faster page loads when frequently used resources are already cached.
Where Can HTTP Responses Be Cached?
HTTP caching is not limited to the browser. A request can pass through multiple caching layers, and each layer can have different responsibilities.
Browser Cache
The browser cache is a private cache associated with a particular user agent. It can store resources such as images, stylesheets, scripts, fonts and API responses when the response permits caching.
A browser cache is particularly useful because a resource that has already been downloaded may not require another network request at all. If the response is still fresh, the browser can reuse it immediately.
Shared Caches
A shared cache can serve responses to multiple users. CDNs and some proxy caches fall into this category.
Shared caching requires additional care because a response generated for one user must not accidentally be served to another user. Personalized responses therefore often require private caching or explicit cache restrictions.
CDN Caching
A content delivery network can cache resources at geographically distributed edge locations. When a user requests a cached resource, the CDN may be able to return it from a nearby location instead of contacting the origin server.
The exact caching behavior depends on the CDN configuration and the HTTP response headers. A CDN can also apply additional rules, but HTTP cache semantics remain an important foundation for predictable behavior.
Freshness and Staleness
One of the central concepts in HTTP caching is freshness. A cached response can generally be reused without contacting the origin while it is considered fresh according to the applicable caching rules.
Once a response becomes stale, that does not necessarily mean it must be discarded immediately. A cache may be able to revalidate the response with the origin server and reuse the stored body if the resource has not changed.
This distinction is important because caching does not have to mean downloading a complete response again every time the freshness lifetime expires.
Cache-Control
Cache-Control is the main HTTP response header used to control caching behavior. It contains directives that tell caches how a response may be stored and reused.
Cache-Control: max-age=3600
The max-age directive specifies how many seconds the response can generally be considered fresh after it has been stored.
max-age
max-age specifies a freshness lifetime in seconds.
Cache-Control: max-age=3600
In this example, the response has a freshness lifetime of one hour.
no-cache
The name no-cache is frequently misunderstood. It does not mean that the response cannot be stored. Instead, it generally means that a stored response must be revalidated before reuse.
Cache-Control: no-cache
This can be useful when you want a browser or cache to retain a response but verify that it is still valid before using it.
no-store
no-store has a much stronger meaning. It tells caches not to store the response.
Cache-Control: no-store
This directive is commonly relevant for responses containing sensitive or highly dynamic information where storing the response is undesirable.
public and private
The public directive indicates that a response may be stored by a shared cache. The private directive indicates that the response is intended for a particular user and should not generally be stored by a shared cache.
Cache-Control: public, max-age=86400
Cache-Control: private, max-age=300
A personalized account page, for example, may contain user-specific information and therefore needs different caching behavior from a public CSS file.
s-maxage
s-maxage specifies a freshness lifetime for shared caches. It can be useful when you want browser and CDN caching to have different lifetimes.
Cache-Control: max-age=60, s-maxage=3600
In this example, a browser can treat the response as fresh for one minute while a shared cache can use it for up to one hour, subject to the applicable caching rules.
must-revalidate
must-revalidate tells a cache that once the response becomes stale, it must not simply reuse the stale response when normal validation is required. The cache needs to follow the applicable revalidation rules.
Cache-Control: max-age=3600, must-revalidate
immutable
The immutable directive can be used for resources that are not expected to change at the same URL during their cache lifetime.
Cache-Control: public, max-age=31536000, immutable
This pattern is particularly useful for versioned or content-hashed static assets where changing the content also changes the URL.
Combining Cache-Control Directives
Cache-Control directives are often combined to describe a complete caching policy.
Cache-Control: public, max-age=31536000, immutable
Another common pattern for a response that can be stored but should be validated before reuse is:
Cache-Control: no-cache
Expires
Expires is an older HTTP response header that specifies an absolute expiration date and time.
Expires: Wed, 30 Sep 2026 12:00:00 GMT
Modern applications generally prefer Cache-Control because it provides more expressive caching directives. Expires can still appear for compatibility with older HTTP infrastructure.
Cache-Control vs Expires
| Header | Type | Typical role |
|---|---|---|
| Cache-Control | Relative directives | Modern control over freshness and caching behavior. |
| Expires | Absolute date | Legacy expiration mechanism. |
When both are present, modern caching implementations use Cache-Control according to its defined semantics rather than treating Expires as the primary freshness mechanism.
ETag
An ETag is an identifier associated with a particular representation of a resource. The server sends it with the response, and a client can later use it to ask whether that representation is still current.
ETag: "abc123"
The exact value of an ETag is determined by the server. It can be based on a content hash, version identifier or another representation-specific value.
Conditional Requests with If-None-Match
After receiving an ETag, a client can send it back in the If-None-Match request header.
If-None-Match: "abc123"
If the server determines that the representation has not changed, it can respond with HTTP 304 Not Modified instead of sending the complete response body again.
HTTP/1.1 304 Not Modified
ETag: "abc123"
A 304 response can therefore save bandwidth even when the browser has to contact the server to validate its cached copy.
Strong and Weak ETags
ETags can be strong or weak. A weak ETag begins with the W/ prefix.
ETag: W/"abc123"
A strong ETag identifies an exact representation for purposes where byte-level identity matters. A weak ETag indicates that the representations are considered equivalent for the relevant caching semantics even though they may not be byte-for-byte identical.
Last-Modified
Last-Modified tells the client when the representation was last modified according to the server.
Last-Modified: Tue, 29 Sep 2026 10:30:00 GMT
A client can later use If-Modified-Since to perform a conditional request.
If-Modified-Since: Tue, 29 Sep 2026 10:30:00 GMT
If the resource has not changed according to the server's modification information, the server can return 304 Not Modified.
ETag vs Last-Modified
| Mechanism | Based on | Typical advantage |
|---|---|---|
| ETag | Representation identifier | Can precisely identify a particular representation. |
| Last-Modified | Modification timestamp | Simple and widely supported validation mechanism. |
A server can provide both ETag and Last-Modified. HTTP caching rules determine how conditional requests are evaluated when multiple validators are available.
What Is a 304 Not Modified Response?
HTTP 304 Not Modified tells the client that its cached representation can be reused because the server has determined that the resource has not changed in the way relevant to the conditional request.
A 304 response normally does not contain the full representation body. The browser combines the validation result with its existing cached response.
HTTP/1.1 304 Not Modified
Cache-Control: max-age=3600
ETag: "abc123"
This is different from serving the resource directly from a fresh cache. A fresh cache hit can avoid contacting the server, while a revalidation requires a request to determine whether the cached response can still be used.
Fresh Cache Hit vs Revalidation
| Situation | Network request to origin | Response body |
|---|---|---|
| Fresh cached response | Usually no | Uses cached body. |
| Stale response with successful validation | Yes | Cached body is reused after 304. |
| Resource changed | Yes | New response body is downloaded. |
| No caching | Yes | New response body is downloaded. |
Cache Busting and Versioned URLs
Long-lived caching works particularly well when resource URLs change whenever their contents change.
/assets/app.8f31c2.js
/assets/app.91ab42.js
/assets/styles.a4c921.cssThe filename can contain a content hash or another version identifier. When the application is rebuilt and the content changes, the generated filename changes as well.
The old URL can remain cached for a long time because it represents an older version. New HTML references the new URL, so users receive the updated asset without requiring a very short cache lifetime.
Caching HTML vs Static Assets
Different resource types often need different caching policies. A static JavaScript file with a content hash can have a long freshness lifetime, while frequently changing HTML may need a much shorter lifetime or regular revalidation.
| Resource | Typical strategy |
|---|---|
| Hashed JavaScript | Long-lived public caching. |
| Hashed CSS | Long-lived public caching. |
| Images | Long-lived caching when URLs are versioned. |
| Fonts | Long-lived caching when versions are controlled. |
| HTML | Shorter caching or revalidation depending on how frequently it changes. |
| Personalized API response | Private caching or no-store depending on sensitivity. |
Caching API Responses
API responses can be cached just like other HTTP responses. The correct policy depends on whether the response is public, personalized, frequently changing or sensitive.
HTTP/1.1 200 OK
Content-Type: application/json
Cache-Control: public, max-age=60
{"status":"ok","version":"1.0"}A public response that is identical for many users can potentially benefit from shared caching. A response containing account-specific information should not accidentally become available to other users through a shared cache.
Caching Personalized Responses
Personalized responses are one of the most important places to think carefully about cache scope. Consider an endpoint that returns information about the currently authenticated user. If a shared cache stores that response incorrectly, another user could potentially receive the cached representation.
Cache-Control: private, max-age=60
For highly sensitive responses, an application may choose not to store the response at all.
Cache-Control: no-store
The Vary Header
Sometimes the response depends on a request header. The Vary response header tells caches which request headers influenced the selected representation.
Vary: Accept-Encoding
This tells a cache that the response can vary depending on the value of Accept-Encoding. A cache therefore should not blindly treat a response generated for one representation as identical to every other representation.
Vary is particularly important when content negotiation, compression or other request-dependent behavior changes the response.
Cache Keys
A cache needs to determine which stored response corresponds to a new request. The combination of request URL and relevant request metadata forms the basis of cache lookup according to the cache's rules.
Query parameters are particularly important. Two URLs such as these can represent different resources:
/products?page=1
/products?page=2Caching infrastructure may also use request headers and other properties when determining whether a stored response can satisfy a request. CDN-specific cache-key configuration can further affect this behavior.
Caching and Cookies
Cookies often indicate that a response is associated with a particular user or session. That does not mean every response involving cookies is automatically uncacheable, but it does mean the caching strategy needs to be examined carefully.
A page that changes based on a session cookie is fundamentally different from a public static asset. Shared caching of personalized responses can create both correctness and privacy problems.
Browser Reload and Cache Behavior
A normal navigation, reload and explicit cache-bypass action can result in different request behavior. Browser developer tools can help reveal whether a resource was served from memory, disk, a service worker, a CDN or the network.
When debugging caching, inspect the actual request and response headers rather than assuming that a resource was or was not cached based only on what appears in the browser.
How to Inspect HTTP Caching
Browser developer tools are one of the most useful ways to debug caching. Open the Network panel, select a request and inspect the response headers and request headers.
- Check Cache-Control.
- Check ETag and Last-Modified.
- Look for If-None-Match and If-Modified-Since on subsequent requests.
- Check whether the response was served from a browser cache.
- Inspect Age when a shared cache or CDN is involved.
- Check Vary when content depends on request headers.
- Inspect CDN-specific cache status headers when available.
The Age Header
The Age response header can indicate the approximate amount of time a response has been resident in a shared cache.
Age: 120
An Age header can be useful when debugging CDN or proxy caching because it provides information that is different from the browser's local cache state.
Cache-Control: no-cache vs no-store
| Directive | Can response be stored? | Requires revalidation? |
|---|---|---|
| no-cache | Yes | Generally yes before reuse. |
| no-store | No | Not applicable because the response should not be stored. |
This distinction is one of the most common HTTP caching mistakes. If the goal is simply to ensure that a stored response is checked before reuse, no-cache may be appropriate. If the goal is to prevent caching altogether, no-store is the relevant directive.
Common HTTP Caching Mistakes
Using Very Short Cache Lifetimes for Everything
Setting max-age to a very small value everywhere can prevent the cache from providing much benefit. Static resources that change only when a deployment occurs do not necessarily need to be revalidated every few seconds.
Using Long Cache Lifetimes Without Versioning
The opposite problem is serving frequently changing resources with a very long freshness lifetime while keeping the same URL. Users can then continue receiving an old resource until the cache expires or is explicitly invalidated.
Confusing no-cache with no-store
As discussed earlier, no-cache does not mean that the response cannot be stored. Using the wrong directive can lead to either unnecessary network traffic or unintended storage.
Caching Personalized Data Publicly
A response containing user-specific data should not be treated as a generic public resource. Incorrect shared caching can result in one user's representation being reused for another user.
Ignoring Cache Headers During Debugging
Developers sometimes change application code repeatedly when the actual problem is an old cached response. Always inspect the request and response headers before concluding that the server is returning the wrong content.
Trying to Solve Every Cache Problem With Purging
CDN purges and cache invalidation tools are useful, but relying on manual purges for every deployment can make a caching architecture harder to operate. Versioned URLs often provide a simpler approach for static assets.
Cache Invalidation
Cache invalidation means making sure that cached content is no longer used when it should not be. It is often described as one of the difficult parts of caching because already distributed copies may exist in multiple locations.
There are several approaches to invalidation. You can use short freshness lifetimes, conditional revalidation, explicit CDN purging or versioned URLs.
| Strategy | Useful for | Trade-off |
|---|---|---|
| Short max-age | Frequently changing content | More network requests. |
| Revalidation | Content that needs freshness checks | Requires a request when validation is needed. |
| CDN purge | Urgent invalidation | Adds operational complexity. |
| Versioned URLs | Static assets | Requires URL/version management. |
HTTP Caching and CDNs
A CDN can dramatically increase the value of HTTP caching by storing responses at edge locations. A user in one region may receive a cached response from an edge server nearby rather than contacting the origin server on every request.
For static assets, a common approach is to combine long cache lifetimes with versioned filenames. HTML and API responses generally require more careful policies because their content can change independently of the asset URL.
When debugging a CDN, distinguish between the browser cache and the CDN cache. A browser may have a resource locally even when the CDN would have returned a different response, and a CDN may have a cached response even though the browser is making a network request.
HTTP Caching in Next.js Applications
Modern frameworks such as Next.js can introduce additional caching layers beyond the browser and CDN. Depending on the application and framework features being used, generated pages, data requests and static assets can have different caching behavior.
This makes it important to distinguish HTTP caching from framework-level caching. A response may be cached by a framework or CDN even when the browser itself has not cached it, and the reverse can also happen.
For static assets generated with content hashes, long-lived browser and CDN caching is generally straightforward because a new build produces a new URL. Dynamic pages and API responses require a policy based on how frequently their underlying data changes.
Recommended Caching Patterns
There is no single Cache-Control value that is correct for every resource. A practical policy starts by identifying whether the response is public or private, how often it changes and whether its URL changes when its content changes.
| Resource type | Example policy |
|---|---|
| Immutable versioned asset | public, max-age=31536000, immutable |
| Frequently changing public resource | public, max-age=60 |
| Resource that should be validated | no-cache |
| Sensitive response | no-store |
| Personalized but cacheable response | private with an appropriate max-age |
These are patterns rather than universal rules. The correct policy depends on the application, data sensitivity, update frequency and caching infrastructure.
A Practical HTTP Caching Checklist
- Identify which responses are safe to cache.
- Separate public resources from personalized responses.
- Use Cache-Control to define the intended caching behavior.
- Use appropriate freshness lifetimes.
- Use ETag or Last-Modified when conditional validation is useful.
- Use versioned URLs for long-lived static assets.
- Avoid public caching for user-specific responses unless the design explicitly supports it.
- Check Vary when representations depend on request headers.
- Inspect browser and CDN behavior separately.
- Use developer tools to inspect actual HTTP headers.
- Test cache behavior after deployments.
- Have an invalidation strategy for resources that can change unexpectedly.
HTTP Caching vs Browser Storage
HTTP caching is different from application storage mechanisms such as localStorage, sessionStorage and IndexedDB. HTTP caching is controlled primarily through HTTP semantics and is designed around HTTP requests and responses.
Application storage, on the other hand, is explicitly accessed by JavaScript and can be used for application-specific state or data. Choosing between them depends on what you are storing and how that data should participate in HTTP requests.
HTTP Caching vs Service Workers
Service workers introduce another layer that can intercept network requests and implement application-defined caching strategies. A service worker can use the Cache API to store responses and decide when to return cached data or contact the network.
This is more programmable than ordinary HTTP caching, but it also introduces more complexity. A service worker's cache is not simply a replacement for correct HTTP cache headers.
How to Debug a Stale Response
When a user sees outdated content, start by determining which layer supplied the response. Check the browser's Network panel and inspect response headers, cache indicators and request headers.
- Check the response URL and query parameters.
- Inspect Cache-Control and Expires.
- Check ETag and Last-Modified.
- Look for If-None-Match and If-Modified-Since.
- Check whether the response was loaded from browser cache.
- Inspect CDN cache status if the site uses a CDN.
- Check Age when available.
- Verify whether a service worker is intercepting the request.
- Check framework-level caching and deployment configuration.
- Verify that the application generated the expected new resource.
Frequently Asked Questions
What is HTTP caching?
HTTP caching stores HTTP responses so that later requests can reuse them without always downloading the resource again. Caches can exist in browsers, proxies, CDNs and other infrastructure.
What does Cache-Control max-age mean?
max-age specifies a freshness lifetime in seconds. For example, max-age=3600 indicates a one-hour freshness lifetime under the applicable HTTP caching rules.
What is the difference between no-cache and no-store?
no-cache allows a response to be stored but requires revalidation before reuse in situations where validation is required. no-store tells caches not to store the response.
What is an ETag used for?
An ETag identifies a particular representation of a resource and can be used with If-None-Match to perform conditional requests. If the representation has not changed, the server can return 304 Not Modified.
What does HTTP 304 Not Modified mean?
304 Not Modified means that the server has determined that the client's cached representation can be reused for the conditional request. The server normally does not send the complete response body again.
Should static assets have a long cache lifetime?
They often can, especially when their URLs contain content hashes or another version identifier. When the content changes, the URL changes as well, allowing old cached copies to remain valid.
Can API responses be cached?
Yes. API responses can use HTTP caching, but the policy must account for whether the response is public, personalized, sensitive and how frequently its data changes.
Helpful HTTP Caching Tools
When working with HTTP caching headers, a Cache-Control Generator can help construct Cache-Control directives for a particular caching policy. An HTTP Header Generator is useful when assembling complete response headers, while an HTTP Header Viewer can help inspect headers returned by a server.
For validator-based caching, an ETag Generator can help create ETag values for testing and development. An HTTP Response Formatter can also make raw HTTP responses easier to inspect when debugging caching behavior and related headers.
Conclusion
HTTP caching reduces unnecessary network requests by allowing browsers, CDNs and other caches to reuse previously generated responses. The most important concepts are freshness, validation and cache scope.
Cache-Control defines the main caching policy, while ETag and Last-Modified provide mechanisms for validating stored representations. A fresh cache hit can avoid a network request completely, while a revalidation can avoid downloading the response body when the resource has not changed.
For static assets, long-lived caching combined with versioned URLs is a common and predictable strategy. Dynamic, personalized and sensitive responses require more careful policies. Once the different cache layers and HTTP headers are understood, caching becomes much easier to design, debug and optimize.