# Caching Functions

> 

`defineCachedFunction` wraps any async function with caching — TTL, stale-while-revalidate (SWR), integrity checks, and automatic deduplication of concurrent in-flight calls.

```ts
import { defineCachedFunction } from "ocache";

const getRepo = defineCachedFunction(
  async (name: string) => {
    const res = await fetch(`https://api.github.com/repos/${name}`);
    return res.json();
  },
  {
    name: "github-repo",
    maxAge: 60, // cache each result for 60 seconds
  },
);

// First call fetches; calls within the next 60s return the cached value
const repo = await getRepo("unjs/ocache");
```

`cachedFunction` is an alias for `defineCachedFunction`.

## How it works

- **TTL** — a cached value is served until `maxAge` seconds pass, then it is re-resolved.
- **Deduplication** — concurrent calls for the same key share a single in-flight resolution, so the underlying function runs once even under a burst of requests.
- **Integrity** — entries are tied to a hash of the function and its options. Change the function body or options and stale entries are automatically ignored.

<important>

`maxAge` defaults to **1 second**. If you want meaningful caching, set it explicitly.

</important>

<note>

SWR is **off by default**. Without it, an expired entry is re-resolved in the foreground (the caller waits for the fresh value). Enable `swr: true` to serve the stale value immediately and refresh in the background — see [Dynamic TTL](#dynamic-ttl) and [Invalidation & Expiration](/guide/invalidation).

</note>

## Options

```ts
const cached = defineCachedFunction(fn, {
  name: "my-fn", // cache key name (defaults to fn.name, then a source hash)
  maxAge: 60, // TTL in seconds (default: 1)
  swr: false, // serve stale while revalidating in background (default: false)
  staleMaxAge: 300, // max seconds a stale value may be served (SWR only)
  getMaxAge: (entry) => entry.value?.expires_in, // per-entry TTL from the value
  base: "/cache", // key prefix — string, or string[] for multi-tier caching
  group: "functions", // key group (default: "functions")
  getKey: (...args) => args[0], // custom cache key from the arguments
  shouldBypassCache: (...args) => false, // when true, skip cache and call fn directly
  shouldInvalidateCache: (...args) => false, // when true, force a fresh resolve
  validate: (entry) => entry.value !== undefined, // reject an entry to re-resolve
  serialize: (entry) => entry.value, // prepare the value for storage
  transform: (entry) => entry.value, // reshape the value before returning
  onError: (error) => console.error(error), // handle cache read/write/SWR errors
});
```

By default, entries are stored in an in-memory store. See [Storage](/guide/storage) to plug in a persistent backend, and [HTTP Handlers](/guide/handler) for caching full HTTP responses.

### staleMaxAge

`staleMaxAge` only takes effect with `swr: true`. It caps how long a stale value may be served while a background refresh runs; after `maxAge + staleMaxAge` the entry is fully expired and the next call resolves in the foreground.

`staleMaxAge: 0` means a stale value is **never** served — once expired, the request blocks on revalidation even with SWR on.

## Cache keys

Each call resolves to a storage key built from `base`, `group`, `name`, and a key derived from the arguments (`getKey`, or a hash of all arguments by default).

The `name` resolves as `opts.name || fn.name || anon_<hash(fn)>`:

- A named function (or one passed via a named binding) gets a stable, human-readable key.
- An anonymous inline function falls back to a hash of its source, so two different inline functions don't collide on one key.

<important>

A source hash can't tell apart two functions with identical source that differ only by closed-over variables — they will share a key and thrash each other. Pass an explicit `name` (or `getKey`) for those.

When purging entries with the standalone [`resolveCacheKeys` / `invalidateCache` / `expireCache`](/guide/invalidation) helpers (which never see `fn`), always pass the same `name` you cached with.

</important>

```ts
// Custom key derived from a single id argument
const getUser = defineCachedFunction(async (id: string) => db.users.find(id), {
  name: "getUser",
  maxAge: 60,
  getKey: (id) => id,
});

await getUser("user-123"); // key includes "user-123"
```

## Dynamic TTL

Some values carry their own expiry — an OAuth token with `expires_in`, an upstream response with a `max-age`. Use `getMaxAge` to derive the lifetime from the resolved value instead of a fixed constant.

It runs after the resolver and returns either a number (seconds, shorthand for `maxAge`) or `{ maxAge?, staleMaxAge? }` to also override the stale window. The returned values override the static options for that entry, driving both the freshness check and the storage TTL.

```ts
const getToken = defineCachedFunction(
  async () => {
    const res = await fetch("https://auth.example.com/token", { method: "POST" });
    return res.json(); // { access_token, expires_in }
  },
  {
    name: "oauth-token",
    // Cache each token for its own lifetime, minus a small safety margin
    getMaxAge: (entry) => Math.max(1, (entry.value?.expires_in ?? 60) - 5),
  },
);
```

<note>

Return `undefined` (or omit a field) to fall back to the static option. A returned value `<= 0` disables caching for that entry (it re-resolves on every access); negatives are clamped to `0`, never treated as "cache forever".

</note>

## Custom serialization

Some resolver outputs can't be persisted as-is — a `ReadableStream`, a class instance. Use `serialize` to convert the value into a storable form on write, and `transform` to reconstruct the usable value on read.

`serialize` runs exactly once per resolution, right after the resolver — even across concurrent deduplicated calls — so consuming a one-shot source such as a stream is safe.

```ts
const getReport = defineCachedFunction(
  () => generateReportStream(), // resolves a one-shot ReadableStream
  {
    name: "report",
    maxAge: 300,
    serialize: (entry) => streamToString(entry.value), // store as a string
    transform: (entry) => stringToStream(entry.value), // rebuild a stream on read
  },
);
```

## Cache status

`transform` receives the entry with a `status` field describing how the value was served on this call — useful for metrics or observability. It is one of:

- `"miss"` — resolved fresh (nothing was cached)
- `"hit"` — a fresh cached value was returned
- `"stale"` — a stale value was served while a background SWR refresh runs
- `"revalidated"` — an expired value was re-resolved in the foreground before returning

```ts
const getData = defineCachedFunction(fetchData, {
  name: "data",
  maxAge: 60,
  transform: (entry) => {
    metrics.increment(`cache.${entry.status}`); // "hit" / "miss" / ...
    return entry.value;
  },
});
```
