Ctrl + K
Servers25 min read

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.

Published: 2026-10-05

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.

ComponentPurpose
Browser cacheStores resources locally on the user's device.
Shared cacheStores responses that can potentially be reused by multiple users.
CDN cacheStores resources at edge locations closer to users.
Reverse proxy cacheCaches 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.
💡 Caching is especially effective for resources that are requested frequently but do not change often, such as images, fonts, CSS files, JavaScript bundles and versioned static assets.

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
⚠️ Do not choose Cache-Control directives based only on their names. In particular, no-cache and no-store have very different meanings. no-cache permits storage but requires revalidation, while no-store tells caches not to store the response.

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

HeaderTypeTypical role
Cache-ControlRelative directivesModern control over freshness and caching behavior.
ExpiresAbsolute dateLegacy 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.

💡 For application development, the important distinction is that an ETag is a validator, not a cache lifetime. Cache-Control determines freshness; ETag helps determine whether a stored response is still valid when revalidation occurs.

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

MechanismBased onTypical advantage
ETagRepresentation identifierCan precisely identify a particular representation.
Last-ModifiedModification timestampSimple 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

SituationNetwork request to originResponse body
Fresh cached responseUsually noUses cached body.
Stale response with successful validationYesCached body is reused after 304.
Resource changedYesNew response body is downloaded.
No cachingYesNew 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.css

The 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.

💡 For static assets, versioned or content-hashed URLs are usually more predictable than relying on aggressive cache invalidation. The URL itself becomes part of the versioning strategy.

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.

ResourceTypical strategy
Hashed JavaScriptLong-lived public caching.
Hashed CSSLong-lived public caching.
ImagesLong-lived caching when URLs are versioned.
FontsLong-lived caching when versions are controlled.
HTMLShorter caching or revalidation depending on how frequently it changes.
Personalized API responsePrivate caching or no-store depending on sensitivity.
⚠️ Do not blindly apply a one-year public cache policy to HTML or personalized API responses. Long-lived shared caching is safe only when the response can actually be reused for the relevant users.

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
⚠️ Never assume that authentication automatically prevents every caching problem. Caches operate according to HTTP caching rules, so sensitive or personalized responses need an explicit and appropriate caching policy.

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=2

Caching 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

DirectiveCan response be stored?Requires revalidation?
no-cacheYesGenerally yes before reuse.
no-storeNoNot 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.

StrategyUseful forTrade-off
Short max-ageFrequently changing contentMore network requests.
RevalidationContent that needs freshness checksRequires a request when validation is needed.
CDN purgeUrgent invalidationAdds operational complexity.
Versioned URLsStatic assetsRequires 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.

💡 When debugging a Next.js application's caching behavior, inspect both the framework configuration and the actual HTTP response headers. Framework-level caching and HTTP caching are related but are not the same mechanism.

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 typeExample policy
Immutable versioned assetpublic, max-age=31536000, immutable
Frequently changing public resourcepublic, max-age=60
Resource that should be validatedno-cache
Sensitive responseno-store
Personalized but cacheable responseprivate 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.

Found an issue?

Found an error, outdated information, or something missing from this article? Let me know through the Contact page.

Your feedback helps improve our articles and keep them accurate and useful.