# HTTP caching

> Source: https://ferron.sh/docs/use-cases/content/caching

The Ferron HTTP response cache stores complete `GET` response representations in memory and serves them directly to clients. This reduces backend load and improves response times. It is especially useful for frequently accessed content like HTML pages, API responses, and static assets.

## Basic HTTP caching

To enable caching for an entire host, use the `cache` directive at the HTTP host level:

```ferron
example.com {
    cache {
        max_response_size 1048576
    }
}
```

This configuration caches responses up to 1MB in size. The default `max_response_size` is 2MB, and the global default `max_entries` is 1024.

> [!important]
>
> - The cache stores only `GET` and `HEAD` requests, and `HEAD` requests reuse cached `GET` representations.
> - The cache never stores responses with `Vary: *`.
> - The cache does not store public responses containing `Set-Cookie`.
> - The in-memory cache clears on server restart, so use an external cache like Redis for persistent caching.

## Caching with Vary headers

The `vary` directive makes sure Ferron caches responses separately based on request headers. This is crucial for content that varies by `Accept-Encoding`, `Accept-Language`, or other headers:

```ferron
example.com {
    cache {
        vary Accept-Encoding Accept-Language
    }
}
```

Without `vary`, Ferron could cache responses with different headers together and serve the wrong content to clients.

> [!tip]
> If you see unexpected cache misses, check that the `vary` headers match your use case. If the cache size keeps growing, check for frequently accessed large responses and consider reducing `max_response_size`.

## Excluding sensitive responses from cache

Use the `ignore` directive to remove headers from cached responses while keeping them in live responses. This is useful for removing `Set-Cookie` from cached content:

```ferron
example.com {
    cache {
        ignore Set-Cookie
    }
}
```

## Disabling cache for specific paths

Override inherited caching settings for specific paths using `location` blocks:

```ferron
example.com {
    cache {
        max_response_size 1048576
    }

    location /admin {
        cache false
    }

    location /api/private {
        cache false
    }
}
```

This disables caching for `/admin` and `/api/private` paths while keeping caching enabled for the rest of the host.

## LSCache-compatible applications

If your upstream application uses LiteSpeed Cache-style headers, enable override mode:

```ferron
example.com {
    cache {
        max_response_size 1048576
        litespeed_override_cache_control

        # Also, emit X-LiteSpeed-Cache response header
        emit_litespeed_headers
    }
}
```

This tells Ferron to prioritize `X-LiteSpeed-Cache-Control` headers over standard `Cache-Control` and `Expires` headers. Ferron uses this when deciding whether to store a response and what TTL to use.

## Caching with authentication

Ferron partitions private responses by client context using the client IP, authenticated username, and detected private cookies. This means authenticated users get personalized cached responses:

```ferron
example.com {
    cache {
        max_response_size 1048576
    }

    location /dashboard {
        basic_auth
        cache {
            max_response_size 1048576
        }
    }
}
```

Each authenticated user has their own cached dashboard pages based on their credentials.

## Caching with reverse proxying

Combine reverse proxying with caching to cache backend responses:

```ferron
example.com {
    location /api {
        proxy http://localhost:3000
        cache {
            max_response_size 524288
            vary Accept-Encoding
        }
    }
}
```

This caches API responses from the backend, reducing load during traffic spikes.

## Stale-while-revalidate

Stale-while-revalidate allows Ferron to serve a cached response after its `max-age` has expired. This works as long as the response falls within the `stale-while-revalidate` window set by the origin. This avoids latency spikes when the cache entry expires and concurrent requests arrive:

```ferron
example.com {
    location /api {
        proxy http://localhost:3000
        cache {
            max_response_size 524288
        }
    }
}
```

The backend controls the stale window with `Cache-Control`:

```http
Cache-Control: public, max-age=10, stale-while-revalidate=300
```

With this configuration:

- The cache stores responses for 10 seconds.
- After 10 seconds, the first request revalidates with the backend and gets fresh content.
- Concurrent requests during revalidation receive the stale response immediately.

```ferron
example.com {
    location /api {
        proxy http://localhost:3000
        cache {
            enable_stale_while_revalidate false
        }
    }
}
```

> [!note]
> Ferron 3 does not support background revalidation. Stale-while-revalidate always involves a synchronous upstream request for one request (the leader). Other concurrent requests see the stale content. This is a known limitation stemming from the absence of internal route invocation in Ferron 3.

## Stale-if-error

Stale-if-error falls back to stale cached content when revalidation encounters a 5xx error. This protects the service against transient backend failures:

```http
Cache-Control: public, max-age=60, stale-if-error=3600
```

If the backend returns a 5xx error during revalidation, Ferron serves the stale cached response. It does not forward the error to the client. This keeps your application running during brief backend outages.

```ferron
example.com {
    location /api {
        proxy http://localhost:3000
        cache {
            enable_stale_if_error false
        }
    }
}
```

## Caching with rate limiting

Use caching alongside rate limiting to protect backend services:

```ferron
example.com {
    location /api {
        ratelimit {
            rate 100
            burst 50
        }
        proxy http://localhost:3000
        cache {
            max_response_size 524288
        }
    }
}
```

Cached responses bypass the rate limiter and backend entirely, providing maximum protection.

## Multi-instance cache purge propagation

Multiple Ferron instances may run behind a load balancer. In that case, a cache purge on one instance does not invalidate entries on other instances. The `purge_propagation` directive solves this. It sends purge events to an external control-plane service. That service broadcasts them to all other registered edge instances.

**Edge instance configuration:**

```ferron
example.com {
    proxy http://backend:3000
    cache {
        purge_method
        purge_allowed_ips "10.0.0.0/8"
        purge_propagation {
            control_plane_url http://control-plane:9090/cache/purge
            shared_secret edge-to-plane-secret
            node_id edge-1
        }
    }
}
```

**How it works:**

1. A `PURGE` request or an `X-LiteSpeed-Purge` header triggers a local cache purge on edge-1.
2. Edge-1 sends a `POST` to the control-plane with the purged path and `origin: "edge-1"`.
3. The control-plane sends `PURGE` requests to edge-2 and edge-3 (skipping edge-1).
4. Each edge purges its local cache and returns `200 OK`.

**Loop prevention:**

- Edges receiving `PURGE` requests with `X-Purge-Source: propagation` execute the purge locally but do not re-propagate.
- The control-plane excludes the origin node from its broadcast list.

**Control-plane webhook protocol:**

```http
POST /cache/purge HTTP/1.1
Host: control-plane:9090
Content-Type: application/json
X-Purge-Secret: edge-to-plane-secret

{
  "path": "/blog/post-123",
  "origin": "edge-1"
}
```

The control-plane must accept this POST, authenticate via `X-Purge-Secret`, and fan out `PURGE` requests to all registered edges except the origin. Ferron does not include a built-in control-plane. Operators can implement one with any HTTP framework.