Halloween DealsAmazon USHalloween week deals: useful picks before month-endAmazon US: current deals, useful picks and tech finds.Check DealsClean PCRecommendedOne scan can reveal what keeps slowing WindowsLook for cleanup and repair opportunities.Run ScanHalloween DealsAmazon USOctober picks are moving - compare before checkoutAmazon US: current deals, useful picks and tech finds.See Picks×
FREEDOM251

Freedom251 › Blog › browser caching

How to Enable Browser Caching on Nginx (with Safe CSS, JavaScript and HTML Policies)

By Freedom251 Team · · 11 min read

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Enable browser caching in Nginx by returning the right HTTP response headers from the location that serves each resource. Use long, immutable freshness for files whose URLs contain a content hash or version, and use no-cache with validators for HTML and other responses that can change at the same URL. Nginx’s expires directive generates Expires and Cache-Control; add_header lets you write an explicit cache policy.

What browser caching in Nginx actually controls

A browser decides whether it can reuse a response from its local cache by reading the response headers sent by your web server. Nginx does not switch on a separate “browser cache” feature. You enable the behavior by adding cache headers to the response produced by the matching server or location block.

As an Amazon Associate I earn from qualifying purchases.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

The two directives used most often are part of Nginx’s ngx_http_headers_module:

  • expires sets an Expires date and also creates a corresponding Cache-Control freshness policy.
  • add_header writes an explicitly chosen response header value, such as Cache-Control: public, max-age=31536000, immutable.

Both directives can be used in the http, server, and location contexts. An add_header can also be used in an if block inside a location, although a normal location rule is easier to audit and less prone to surprises.

“Fresh for one year” is not automatically safe for every file. If a browser stores /app.js for 365 days and you deploy new bytes at that same URL, visitors can continue running the old script. Long caching is appropriate when the URL changes whenever the bytes change—for example, app.abc123.js. A stable entry document such as index.html normally needs revalidation so it can point users to the newest asset URLs.

Choose a policy for each kind of response

Decide how a URL changes before choosing its lifetime. The table below gives practical starting points; the right value depends on your deployment process and how much delay is acceptable when content changes.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
Resource Example policy When it is appropriate
Fingerprint-named CSS, JavaScript, images and fonts public, max-age=31536000, immutable The filename contains a content hash or release version, so a changed file is published at a new URL. The 31536000 value is one year in seconds and is a configuration example, not a measured performance guarantee.
Unversioned CSS or JavaScript Short max-age or no-cache The same URL may receive new bytes. A short lifetime limits staleness; no-cache stores the response but requires a validation check before reuse.
HTML entry documents no-cache with ETag and/or Last-Modified The browser can keep a local copy while checking whether the document changed. An unchanged representation can be returned as 304 Not Modified without sending the body again.
Personalized or sensitive responses An application-appropriate private or no-store policy User-specific data should not be made publicly reusable by shared caches. Select the directive that matches your privacy and session requirements.

The important distinction is that no-cache does not mean “never store.” It means “do not reuse the stored representation without checking with the origin.” no-store is the directive used when a response should not be retained at all.

Prepare the Nginx configuration

Use the configuration file loaded by the active Nginx instance, and first identify the root and location that serve the files. A request such as /assets/app.abc123.js might be handled by a dedicated location /assets/, a regular-expression location, or a broader location /. Put the policy in the block that actually serves the response.

Before changing production behavior, make a copy of the current configuration and check which file is loaded on your system. A syntactically valid rule in an unused virtual host will have no effect, while a broad rule in the wrong server block can affect unrelated sites.

After editing, always run:

nginx -t

Proceed only when the test reports that the configuration syntax is successful. Reload Nginx through the service manager used by your host so existing connections can finish cleanly; common installations use a command equivalent to systemctl reload nginx. If your platform uses a different manager, use its normal reload operation rather than stopping the server.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Baseline configuration for hashed assets and HTML

This example separates entry documents from static files. The regular-expression locations are case-insensitive because of ~*. Adjust the document root and extensions to match your application.

server {
    root /var/www/example;

    # Documents can change at the same URL; validate before reuse.
    location ~* .html$ {
        expires -1;
        add_header Cache-Control "no-cache";
    }

    # Use this only when these URLs contain a content hash or release version.
    location ~* .(css|js|png|jpg|jpeg|gif|svg|webp|woff2?)$ {
        expires 1y;
        add_header Cache-Control "public, immutable";
    }
}

expires 1y creates a positive freshness lifetime and a future Expires value. The additional Cache-Control header marks the response as publicly cacheable and immutable while it is fresh. Use this pattern only when deployment changes the URL along with the bytes.

A negative duration such as expires -1 generates a past expiration and a no-cache cache-control policy. If you need one precisely controlled header value, prefer an explicit add_header rule and inspect the result so you do not accidentally emit conflicting or duplicate policies. The essential requirement is that the final response seen by the browser expresses one intentional policy.

expires versus add_header

Use expires for duration-based policies

expires is convenient when your policy is naturally described as a duration. Examples include expires 1y for a year, expires 7d for a week, or expires -1 for a revalidation policy. Nginx calculates the HTTP date and the corresponding freshness value for you.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Nginx also supports special values such as epoch, max, and off. off disables the module’s automatic modification of the expiration fields in that context. Treat these as configuration controls, not as substitutes for deciding whether a URL is versioned or personalized.

Use add_header for an explicit Cache-Control value

add_header is useful when you want to state the complete policy directly, including directives such as public, private, immutable, must-revalidate, or no-store. For example:

location /assets/ {
    add_header Cache-Control "public, max-age=604800";
}

Here, 604800 is seven days in seconds. This example deliberately does not claim a year of freshness; it is suitable for an unversioned asset directory only when a week of possible staleness is acceptable. If the directory contains fingerprinted files, a longer lifetime can reduce repeat downloads, provided the URL-changing rule is enforced.

Understand the always parameter

By default, Nginx applies an add_header only to response status codes in the module’s normal list. Add always when the header must also be attached to statuses outside that list, such as an error response:

Free tools Windows power users keep installed

One-click scans. No signup required.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
location /assets/ {
    add_header Cache-Control "public, max-age=604800" always;
}

Use this intentionally. A cache policy on an error page is different from a policy on a successful asset, and caching an unexpected error for too long can hide a recovery. Test both successful and error responses when you depend on always.

Place rules where Nginx will apply them

Nginx selects a location by matching the request URI, then maps that URI to a file using the applicable root. A cache rule belongs in the most specific block that serves the resource. For example, a dedicated location /assets/ is easier to reason about than a server-wide rule that also catches HTML, API responses, or downloads.

Header inheritance is a frequent source of surprises. An add_header defined at a higher level is inherited only when the current level has no add_header directives of its own. Adding one header inside a nested location can therefore prevent other headers from the parent from being inherited. Review the complete set of security and cache headers after introducing a nested rule, and repeat them in the child location when that is the intended result.

Regular-expression locations can also win over a prefix location depending on Nginx’s matching rules. If a request is not receiving the policy you expect, inspect the effective configuration and test the exact URI rather than assuming the visually nearest block handled it.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Make long caching safe with URL fingerprinting

An immutable policy works because the URL becomes a release identifier. A build system may transform app.js into app.abc123.js, where abc123 represents the content or a build version. When the source changes, the next deployment emits a different name, such as app.98f7de.js. Browsers can keep the old file for its full lifetime because the HTML or manifest will reference the new URL.

The deployment sequence matters:

  1. Build the new assets and generate their fingerprinted filenames.
  2. Publish the new files before exposing HTML that references them.
  3. Deploy the updated HTML or manifest so new visitors discover the new URLs.
  4. Retain old files long enough for already-cached HTML or in-flight sessions to request them, or provide a deliberate rollback strategy.

Do not apply max-age=31536000 and immutable to a stable index.html URL merely because it is small. If that document changes but its URL does not, a browser can keep presenting an obsolete application shell until the year-long freshness period expires or the user performs a cache-clearing action.

Use validators for HTML and changing resources

Revalidation is efficient when the server can identify whether the representation changed. Nginx or the upstream application may provide an ETag, a Last-Modified date, or both. A browser that has a stored response sends If-None-Match and/or If-Modified-Since. If the representation is unchanged, the server returns 304 Not Modified with no response body; the browser reuses its stored bytes.

This saves bandwidth while still allowing an entry document to update promptly. A no-cache response can therefore be a good default for HTML: storage is allowed, but reuse is conditional on a check. If an application generates personalized HTML, add the appropriate privacy directives instead of making the response public.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Validators are not a replacement for correct URL versioning. They handle changes at a stable URL, whereas fingerprinting lets immutable assets avoid revalidation altogether. Many sites use both approaches: revalidated HTML points to immutable CSS, JavaScript, images, and fonts.

Apply and verify the configuration

Use a representative fingerprinted asset and the entry document when testing. A successful configuration change should be observable in the actual response headers, not inferred from a browser setting.

  1. Run nginx -t and fix every reported error before reloading.
  2. Reload Nginx with the host’s normal service manager.
  3. Inspect an asset response:
curl -I https://example.test/assets/app.abc123.js

Look for the intended Cache-Control value and an Expires date when you used expires. Then inspect the entry document:

curl -I https://example.test/

Confirm that the document is not accidentally receiving the long-lived asset policy. Header order and formatting can vary, but the effective directives must match the location that handled the request.

What’s actually slowing this PC down?

Pick the symptom - the matching free tool is one click away.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

To test revalidation, first capture the validator from the response. For an ETag, send it back in a conditional request:

curl -I -H 'If-None-Match: "your-etag-value"' https://example.test/

For a last-modified validator, use the value returned by the server:

curl -I -H 'If-Modified-Since: your-last-modified-value' https://example.test/

An unchanged representation should produce 304 Not Modified. A 200 OK response can be correct when the validator is absent, malformed, expired, or no longer matches the current representation.

Test a URL with a fingerprint and one without it. This catches an overly broad regular-expression location that gives unversioned files an unsafe year-long lifetime. If you rely on always, request a known error path as well as a successful file and verify whether the header appears on both.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
Independent reader supportYour contribution helps us test, update, and keep practical guides available for everyone.Support on Ko-Fi

Why the browser may still download the same asset

The request is not hitting the location you edited

Check the exact hostname, URI, and virtual host. A CDN, load balancer, or another Nginx server may be responding instead of the instance whose configuration you changed. Compare headers from the public hostname with a direct origin request when your architecture allows it.

A proxy or application is replacing the headers

An upstream application, reverse proxy, or CDN can add, replace, or strip Cache-Control, Expires, ETag, or Last-Modified. Inspect the headers at the browser-facing endpoint. The policy that matters is the one the user agent receives, not necessarily the one emitted by an internal hop.

The file URL is stable but the bytes changed

A long-lived response at /main.js remains fresh even after you overwrite the file. Rename the asset when its content changes, or shorten the policy and rely on validation. Updating only the file on disk cannot invalidate copies already stored by browsers.

You are confusing reload behavior with normal navigation

Developer tools often provide options such as disabling the cache while the panel is open. A hard reload can also request resources differently from an ordinary navigation. Test in a normal browsing session and inspect the request and response headers before changing Nginx rules.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

A nested header changed inheritance

If a child location contains one add_header, parent-level headers may no longer be inherited there. Compare the effective headers for URLs handled by the parent and child locations, then define the complete intended set at the level that serves the response.

Browser cache and Nginx proxy cache are different layers

Browser caching is client-side and is controlled by headers delivered to the user agent. Nginx proxy caching is server-side storage of responses obtained from an upstream application. Enabling one does not enable the other.

For Nginx proxy caching, configure a cache zone with proxy_cache_path in the http context, select it with proxy_cache in the relevant proxy location, and define freshness with directives such as proxy_cache_valid. Applications commonly also need bypass or no-cache conditions for logged-in sessions, cookies, authorization headers, or other personalized requests.

A response can be stored by a browser while not being stored by Nginx, or stored by Nginx while the browser revalidates every time. Diagnose the layer that is failing by checking the browser-facing headers and, separately, the proxy cache status and upstream request path.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Privacy, security and operational safeguards

  • Do not mark user-specific pages or API responses public unless you have verified that shared caches cannot serve one user’s representation to another.
  • Use no-store for responses that must not remain in browser storage, such as particularly sensitive data, when that matches the application’s security requirements.
  • Keep HTML revalidation-friendly so a deployment can change asset references without waiting for an old document to expire.
  • Only use immutable when the URL is guaranteed to change with the content. It is a promise that revalidation is unnecessary while the response is fresh.
  • Document the cache policy alongside the release process. A future developer who changes the build output or filename scheme must know whether the one-year rule is still safe.
  • After any CDN, framework, or reverse-proxy change, recheck the final public headers; an upstream layer may override Nginx’s values.

Production checklist

  • Identify the exact location serving each class of resource.
  • Use long freshness only for fingerprinted or otherwise versioned URLs.
  • Use no-cache plus ETag and/or Last-Modified for HTML that changes at a stable URL.
  • Choose either a duration-based expires policy or a deliberately explicit Cache-Control value, and inspect for duplicate or conflicting headers.
  • Review header inheritance whenever you add an add_header in a nested location.
  • Run nginx -t, reload safely, and inspect representative responses with curl -I.
  • Send conditional requests and confirm 304 Not Modified for unchanged, revalidated resources.
  • Test both versioned and unversioned URLs, plus error responses when using always.
  • Check the browser-facing endpoint for CDN or upstream header overrides.

With those checks in place, Nginx can keep immutable static assets out of repeat downloads while still allowing HTML and changing responses to update promptly.

Product prices and availability are accurate as of the date/time indicated and are subject to change. Any price and availability information displayed on Amazon at the time of purchase will apply.

browser cachingCache-ControlExpiresHTTP-CachingNGINXweb performance

Leave a Reply

Your email address will not be published. Required fields are marked *

Free tools Windows power users keep installed

One-click scans. No signup required.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Recommended PC Tool
Recommended PC Tool
Windows Errors? Fix Them Before They SpreadFree repair scan
Crashes, No Sound, or Screen Glitches?Free driver scan

Two free Windows tools

One Free Minute Could Fix That PC

Before you go - each of these free tools takes about a minute and tackles what quietly slows a Windows PC down.

Special offer. View Outbyte info, uninstall instructions, EULA, and Privacy Policy.