# Cache-Control & Eligibility

> 

`defineCachedHandler` synthesizes a `Cache-Control` header for cacheable responses, but it always defers to what the handler sets and to a set of built-in eligibility rules. This page covers how to keep responses out of the cache, decouple server-side storage from client caching, and add your own eligibility rules.

## What is never cached

Before storing a response, ocache applies these built-in checks. A response failing **any** of them is still returned to the caller, but is never stored or served from the cache:

- **4xx / 5xx statuses** — only successful responses are cached.
- **Cache-Control: no-store or private** — an explicit opt-out from the handler.
- **Empty body** — nothing to replay.
- **Missing etag / last-modified** — required for conditional requests (ocache adds these itself when the handler omits them, so this mainly guards handler-set placeholder values).

These checks always apply and cannot be turned off.

## Private / non-cacheable responses

To keep a specific response out of the cache from inside the handler, set `no-store` or `private`:

```ts
const handler = defineCachedHandler(
  async (event) => {
    const user = await getUser(event);
    if (!user) {
      return new Response("Please log in", { status: 401 });
    }
    // Personalized — never cache this one
    return new Response(renderDashboard(user), {
      headers: {
        "content-type": "text/html",
        "cache-control": "private",
      },
    });
  },
  { maxAge: 300 },
);
```

Any **other** `Cache-Control` the handler sets is preserved verbatim — ocache only synthesizes `max-age` / `s-maxage` / `stale-while-revalidate` when the handler didn't set a `Cache-Control` of its own.

<important>

`no-store` / `private` only controls what gets **stored** — it does not partition the cache key. Concurrent requests are still coalesced by cache key, so responses that differ per user must be keyed correctly (via [`varies`](/guide/handler) or [`allowCookies`](/guide/cookies)) or a personalized body could be shared.

</important>

## Server-only caching (`sendCacheControl`)

Sometimes you want to cache a response **in your own storage** (to save recomputing it) while telling browsers and CDNs *not* to cache it — for example a page that is expensive to build but must always be revalidated downstream. Reaching for `no-store` / `private` doesn't work here: those also disqualify the response from storage.

Set `sendCacheControl: false` to decouple the two. The response is still stored and served from cache (SWR, `etag`, and `last-modified` all still apply), but no `Cache-Control` header is emitted:

```ts
const handler = defineCachedHandler(myHandler, {
  maxAge: 60,
  swr: true,
  sendCacheControl: false, // stored & served from cache, but nothing advertised downstream
});
```

This only governs ocache's own synthesis — a `Cache-Control` the handler sets explicitly is still preserved and sent.

<tip>

If `invalidateCache`/`expireCache` clears your storage but callers still get stale responses, a browser or CDN in front of the handler is likely serving from its own copy, cached independently via the synthesized `Cache-Control` — a purge to server storage never reaches it. `sendCacheControl: false` avoids this by never letting the client/CDN cache the response in the first place.

</tip>

## Custom eligibility (`shouldCache`)

Use `shouldCache` to add your own rejection rule on top of the built-in checks. It receives the serialized response and returns `false` to skip caching (the response is still returned to the caller). For example, to keep `3xx` redirects out of the cache, which the built-in checks would otherwise allow:

```ts
const handler = defineCachedHandler(myHandler, {
  maxAge: 60,
  shouldCache: (res) => res.status < 300 || res.status >= 400,
});
```

Key behaviors:

- **ANDed with the built-ins.** `shouldCache` can only *narrow* what gets cached — it can never force-cache a response the built-in checks reject.
- **May be async.** Return a `boolean` or a `Promise<boolean>`.
- **Runs on reads too.** It gates both storing a fresh response and serving a stored one (including the stale-while-revalidate serve decision), so keep it fast and decide only from the passed entry.
- **Fails closed.** A throwing hook is treated as non-cacheable and reported via `onError`.

## Related

- [`headersOnly`](/guide/handler) skips response storage entirely and only answers conditional (`304`) requests — reach for it when you want the `304` shortcut but no full-body caching.
- [Incremental Static Regeneration (ISR)](/guide/isr) builds on `swr` + `maxAge` to serve stale content while revalidating.
