Frontend Development 21 min read

Master Browser Caching: HTTP Headers, Spring MVC Code, and Nginx Configuration

This article explains how browsers cache resources, the role of HTTP response headers such as Last-Modified, Expires, Cache-Control, ETag, and Age, and provides practical examples of controlling cache behavior with Spring MVC code and detailed Nginx configuration for both static and proxy caching.

Efficient Ops
Efficient Ops
Efficient Ops
Master Browser Caching: HTTP Headers, Spring MVC Code, and Nginx Configuration

Introduction

Many people ask about browser caching; this article explains how browsers cache resources, the relevant HTTP response headers, and how to control caching from Java Spring MVC and Nginx.

Browser cache basics

When a browser requests a page, the server can include caching headers (Last-Modified, Expires, Cache-Control). The browser stores the response and may later validate it with an If-Modified-Since request; a 304 status means the cached copy is still fresh.

Example: first request to http://item.jd.com/1856588.html returns full headers; after pressing F5 the server returns a 304 response.

Controlling cache in Java (Spring MVC)

Sample controller demonstrates setting Last-Modified, Expires and Cache-Control headers.

@RequestMapping("/cache")
public ResponseEntity<String> cache(HttpServletRequest request,
                                 @RequestParam("millis") long lastModifiedMillis,
                                 @RequestHeader(value = "If-Modified-Since", required = false) Date ifModifiedSince) {
    if (ifModifiedSince != null && ifModifiedSince.getTime() == lastModifiedMillis) {
        return new ResponseEntity<>(HttpStatus.NOT_MODIFIED);
    }
    DateFormat gmtDateFormat = new SimpleDateFormat("EEE, d MMM yyyy HH:mm:ss 'GMT'", Locale.US);
    String body = "<a href=''>点击访问当前链接</a>";
    MultiValueMap<String, String> headers = new HttpHeaders();
    headers.add("Last-Modified", gmtDateFormat.format(new Date(lastModifiedMillis)));
    headers.add("Date", gmtDateFormat.format(new Date()));
    long maxAge = 20;
    headers.add("Expires", gmtDateFormat.format(new Date(System.currentTimeMillis() + maxAge)));
    headers.add("Cache-Control", "max-age=" + maxAge);
    return new ResponseEntity<>(body, headers, HttpStatus.OK);
}

When the client sends If-Modified-Since that matches the stored last-modified time, the controller returns HttpStatus.NOT_MODIFIED (304).

First request

http://localhost:9080/cache?millis=1471349916709

returns status 200 with headers:

Last-Modified – document’s last modification time.

Expires – HTTP/1.0 expiration time.

Cache-Control – max-age in seconds (e.g., 20).

Pressing F5 sends

If-Modified-Since

and receives 304; Ctrl+F5 forces a fresh fetch with

Cache-Control: no-cache

and

Pragma: no-cache

.

Additional response headers

Age – time the object has been cached in a proxy (CDN).

Vary – indicates which request headers affect the cached representation (e.g., Accept-Encoding).

Via – shows the proxy chain the response passed through.

ETag – entity tag used for strong or weak validation.

Example ETag controller returns a weak ETag and uses

If-None-Match

for validation.

@RequestMapping("/cache/etag")
public ResponseEntity<String> cache(HttpServletRequest request,
                                 @RequestHeader(value = "If-None-Match", required = false) String ifNoneMatch) {
    long now = System.currentTimeMillis();
    long maxAge = 10;
    String body = "<a href=''>点击访问当前链接</a>";
    String etag = "W/\"" + md5(body) + "\"";
    if (StringUtils.equals(ifNoneMatch, etag)) {
        return new ResponseEntity<>(HttpStatus.NOT_MODIFIED);
    }
    DateFormat gmtDateFormat = new SimpleDateFormat("EEE, d MMM yyyy HH:mm:ss 'GMT'", Locale.US);
    MultiValueMap<String, String> headers = new HttpHeaders();
    headers.add("ETag", etag);
    headers.add("Date", gmtDateFormat.format(new Date(now)));
    headers.add("Cache-Control", "max-age=" + maxAge);
    return new ResponseEntity<>(body, headers, HttpStatus.OK);
}

Summary of caching rules

Server‑sent Last-Modified is checked with If-Modified-Since ; unchanged content yields 304.

Cache-Control:max-age and Expires define how long the browser may keep the copy.

HTTP/1.1 Cache-Control overrides HTTP/1.0 Expires .

Typical Expires value equals current time plus max-age .

ETag provides an alternative validation mechanism.

Nginx cache configuration

Nginx can set

expires

,

etag

and

if-modified-since

directives for static resources.

location /img {
    alias /export/img/;
    expires 1d;
}

When used as a reverse proxy, Nginx can add

proxy_cache

directives to cache upstream responses.

proxy_buffering on;
proxy_cache_path /export/cache/proxy_cache levels=1:2 keys_zone=cache:512m inactive=5m max_size=8g use_temp_path=off;
location = /cache {
    proxy_pass http://backend_tomcat/cache$is_args$args;
    proxy_cache cache;
    proxy_cache_key $scheme$proxy_host$request_uri;
    proxy_cache_valid 200 5s;
    add_header cache-status $upstream_cache_status;
}

Cache status values include HIT, MISS, EXPIRED, UPDATING, STALE, REVALIDATED, BYPASS.

Additional directives such as

proxy_cache_min_uses

,

proxy_no_cache

,

proxy_cache_bypass

,

proxy_cache_use_stale

, and

proxy_cache_lock

fine‑tune caching behavior and prevent cache stampedes.

Cache invalidation

For non‑Plus Nginx, the

ngx_cache_purge

module can purge specific keys.

location ~ /purge(/.*) {
    allow 127.0.0.1;
    deny all;
    proxy_cache_purge cache$1$is_args$args;
}
nginxhttp headersSpring MVCcache controlbrowser caching
Efficient Ops
Written by

Efficient Ops

This public account is maintained by Xiaotianguo and friends, regularly publishing widely-read original technical articles. We focus on operations transformation and accompany you throughout your operations career, growing together happily.

0 followers
Reader feedback

How this landed with the community

login Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.