Skip to content

Http Caching

DodaTech 2 min read

title: "HTTP Caching — Cache-Control, ETag, and Expires Headers" description: "HTTP caching uses Cache-Control, ETag, and Expires headers to instruct browsers and proxies how long to cache responses under the HTTP/1.1 specification." date: 2026-06-28 lastmod: 2026-06-28 weight: 12 tags: [apis, caching] }

HTTP caching relies on Cache-Control, ETag, and Expires headers to define caching policies, enabling browsers and intermediate proxies to serve cached responses.

What You'll Learn

  • The HTTP caching model
  • Key caching response headers
  • Freshness vs validation

Why It Matters

Proper HTTP caching is the foundation of web performance. Correct headers reduce bandwidth, improve latency, and lower origin server load.

HTTP Caching Cycle

flowchart TD
    subgraph Fresh
        C[Client Request] -->|Has fresh cache| C2[Serve from Cache]
    end
    subgraph Stale
        C -->|Cache expired| V[Validate with ETag]
        V -->|304 Not Modified| V2[Use Stored Response]
        V -->|200 + New Body| O[Origin Server]
    end

Code Examples

# Setting cache headers in Flask
@app.route('/api/users')
def get_users():
    response = jsonify(users)
    response.headers['Cache-Control'] = 'public, max-age=3600'
    response.headers['ETag'] = generate_etag(users)
    response.headers['Expires'] = (datetime.utcnow() + timedelta(hours=1)).strftime(
        '%a, %d %b %Y %H:%M:%S GMT'
    )
    return response

# Conditional request handling
@app.route('/api/users')
def get_users_conditional():
    users = db.get_users()
    current_etag = hashlib.md5(str(users).encode()).hexdigest()

    if request.headers.get('If-None-Match') == current_etag:
        return '', 304

    response = jsonify(users)
    response.headers['ETag'] = current_etag
    response.headers['Cache-Control'] = 'public, max-age=3600'
    return response
// Node.js Express caching
app.get('/api/products', (req, res) => {
  const products = db.getProducts();
  const etag = crypto.createHash('md5').update(JSON.stringify(products)).digest('hex');

  if (req.headers['if-none-match'] === etag) {
    return res.status(304).end();
  }

  res.set({
    'Cache-Control': 'public, max-age=3600',
    'ETag': etag,
    'Expires': new Date(Date.now() + 3600000).toUTCString()
  });
  res.json(products);
});

Common Mistakes

1. No Cache-Control Header

Browsers may cache arbitrarily without explicit directives.

2. Overly Long max-age

Stale data persists for too long. Set appropriate TTLs.

3. Missing ETag

Without ETag, clients must re-download unchanged resources.

4. Using Last-Modified Instead of ETag

Last-Modified has second granularity; ETag supports content hashing.

5. Ignoring Vary Header

Different clients may get wrong cached responses without Vary.

Practice Questions

  1. What does Cache-Control: public mean?
  2. How does ETag improve caching?
  3. What is the difference between Expires and max-age?
  4. What is a 304 response?
  5. Why is Vary header important?

Answers:

  1. Any cache (browser, proxy, CDN) may store the response.
  2. ETag enables conditional requests, avoiding re-download of unchanged content.
  3. Expires is absolute (HTTP/1.0); max-age is relative (HTTP/1.1, preferred).
  4. Not Modified — the client's cached version is still valid.
  5. It tells caches to vary the cached response by specified headers (e.g., Accept-Encoding).

Challenge: Implement ETag-based conditional responses for a JSON API endpoint. Measure bandwidth savings with and without ETag.

FAQ

What is the difference between private and public cache?

: Private cache is browser-only; public cache also includes proxies and CDNs.

Can I use both Cache-Control and Expires?

: Yes, but Cache-Control takes precedence over Expires.

How does a CDN use HTTP caching headers?

: CDNs respect origin Cache-Control and Expires headers, storing responses at edge locations.

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro