Agent Native

netlify-caching

This skill enables caching of dynamic responses from Netlify Functions and Edge Functions on Netlify's CDN.

by NetlifyOfficial publisherNot reviewed

Identity proven by GitHub's verified organisation badge (netlify)

Agents
Claude Code, Codex, Cursor, Gemini CLI, GitHub Copilot, OpenCode, claude.ai and API
License
MIT (permissive)
Pin
b5894c0pinned Oct 9, 2026
Context cost
about 3,600 tokenswhen the skill loads
GitHub stars
39on netlify/context-and-tools
Scripts
None found

What it does

“Cache dynamic and static responses on Netlify's CDN from Functions, Edge Functions, and proxies. Use when you add caching or cache-control headers to a function response, tune cache TTL or stale-while-revalidate, set up the durable cache, vary a cache key by query/header/cookie/country/language, purge or invalidate the cache by site or cache tag, use the programmatic Cache API (caches.open/match/put) or @netlify/cache helpers (fetchWithCache/cacheHeaders/getCacheStatus), speed up an expensive API call, add ISR or on-demand revalidation, or debug why a response is or isn't cached via the Cache-Status header.”
The author's description, from SKILL.md

Generated summary from the skill's own text

This skill enables caching of dynamic responses from Netlify Functions and Edge Functions on Netlify's CDN. It provides tools to set cache headers, vary cache keys by query parameters or headers, purge cached content by tag or site, and debug cache behavior using the Cache API and helper utilities.

When to use it

  • Reducing invocation costs by caching expensive API responses or function results
  • Setting up incremental static regeneration or on-demand cache invalidation
  • Varying cached responses based on query params, cookies, country, or language
  • Debugging cache hits and misses via Cache-Status headers on deployed previews

When not to

  • Local development with netlify dev, where CDN caching is not emulated
  • Caching POST/PUT or non-GET requests, which are never cached regardless of headers
  • On-demand Builders, which do not support these caching headers or vary options

Install

These commands install from the publisher's repository and need no account.

skills CLI

npx skills add netlify/context-and-tools --skill netlify-caching

Installs from the repository's default branch, which may have changed since the commit we checked.

GitHub CLI 2.90 or later

gh skill install netlify/context-and-tools netlify-caching@b5894c0e1c16d924dd5b28546b11c2de650fce7e

Installs the commit we checked (b5894c0).

Copy by hand

Download the folder at b5894c0 and put it in .claude/skills/netlify-caching for Claude Code and in .agents/skills/netlify-caching for Codex, Cursor, Gemini CLI, GitHub Copilot and OpenCode.

Skill folder on GitHub at b5894c0

Claude Code plugin

/plugin install netlify-skills@claude-plugins-official

The publisher's plugin in Claude's official marketplace. It may hold more than this skill, and we have not pinned it.

Copy for your agent

A prompt for any coding agent. It installs this skill from GitHub at the pinned commit, copies the files as they are, and runs nothing unless you ask.

Prompt

Install the agent skill "netlify-caching" by Netlify.

Source, pinned to commit b5894c0e1c16d924dd5b28546b11c2de650fce7e:
https://github.com/netlify/context-and-tools/tree/b5894c0e1c16d924dd5b28546b11c2de650fce7e/agent-plugin/skills/netlify-caching

1. Download that folder at exactly this commit. Do not use the default branch.
2. Copy every file unchanged, keeping the folder structure, into .claude/skills/netlify-caching/ for Claude Code and .agents/skills/netlify-caching/ for Codex, Cursor, Gemini CLI, GitHub Copilot and OpenCode. Create the folders if they do not exist.
3. Do not edit the files, and do not run any script in the folder unless I ask you to.
4. When you are done, list the files you copied and where you put them.

Listing and checks: https://www.agentnative.dev/skills/netlify/netlify-caching

SKILL.md at the pin

---
name: netlify-caching
description: Cache dynamic and static responses on Netlify's CDN from Functions, Edge Functions, and proxies. Use when you add caching or cache-control headers to a function response, tune cache TTL or stale-while-revalidate, set up the durable cache, vary a cache key by query/header/cookie/country/language, purge or invalidate the cache by site or cache tag, use the programmatic Cache API (caches.open/match/put) or @netlify/cache helpers (fetchWithCache/cacheHeaders/getCacheStatus), speed up an expensive API call, add ISR or on-demand revalidation, or debug why a response is or isn't cached via the Cache-Status header.
---

# Netlify caching

## Cache-control header to reach for

Dynamic responses (Functions, Edge Functions, proxies) are **NOT cached by default** — you must opt in. Set `Netlify-CDN-Cache-Control` on the response:

```ts
import type { Context } from "@netlify/functions";

export default async (req: Request, context: Context) => {
  return new Response("Hello world", {
    headers: {
      'Netlify-CDN-Cache-Control': 'public, durable, max-age=60, stale-while-revalidate=120'
    }
  });
};
```

Header choice (most specific wins; `CDN-Cache-Control`/`Cache-Control` always pass downstream):
- `Netlify-CDN-Cache-Control` — Netlify CDN only. **Reach for this.**
- `CDN-Cache-Control` — all CDNs that support it.
- `Cache-Control` — any CDN or the browser.

**Legacy path to avoid:** On-demand Builders do **not** support these headers or `Netlify-Vary` — they use a TTL pattern and key on URL path only. Don't reach for ODBs in new code.

## Footguns (read first)

- **Only `GET` is cached.** POST/PUT/etc. are never cached regardless of headers — expose cacheable data on a GET route (inputs in the URL or query string).
- **`netlify dev` does not emulate the CDN cache.** A local cache miss every time is expected. Verify caching on a deployed URL (Deploy Preview or production) via its `Cache-Status` header.
- **Without `Netlify-Vary: query=...`, the full query string is the cache key** — every distinct query string (`utm_*`, `fbclid`, …) is a separate cache entry. Enumerate only the params that change the response.
- **Static assets are fresh for up to a year** — a shorter `max-age` is ignored. They change only on a new deploy or manual purge.
- **basic-auth on ANY page disables caching for the ENTIRE site.**
- **`durable` is serverless-only** — it has no effect on Edge Function responses.
- Never opt sensitive content out of automatic invalidation — it can stay publicly cached after deploys/firewall changes.

## Directives

- `public` cache it / `private` browser-only, not Netlify's shared cache / `no-store` don't cache.
- `s-maxage=N` seconds in Netlify's shared cache (overrides `max-age` there).
- `max-age=N` seconds in any cache.
- `stale-while-revalidate=N` serve stale for N seconds after expiry while revalidating in background.
- `durable` (serverless only) store in Netlify's durable cache so other edge nodes reuse it instead of re-invoking the function.

Defaults when no header is set — static: `Netlify-CDN-Cache-Control: public, s-maxage=31536000, must-revalidate`; dynamic: `Cache-Control: public, max-age=0, must-revalidate`.

## Cache key variation — `Netlify-Vary`

Comma-delimited instructions on the response; pipe-delimited value lists:

```
Netlify-Vary: query=item_id|page, country=es+de|us, cookie=ab_test|is_logged_in
```

- `query=a|b` subset, or bare `query` for all params. Keys case-sensitive; param order irrelevant.
- `header=Device-Type|App-Version` — custom + most standard headers.
- `language=en|es+pt` — `+` groups; checked against `Accept-Language` with quality weighting.
- `country=us|es+pt` — GeoIP, ISO 3166-1 two-letter codes; `+` groups.
- `cookie=ab_test|is_logged_in` — target specific keys, not the whole `Cookie` header.

**Cannot vary by header on:** `Accept*`, `Cache-Control`, `Connection`, `Content-Length`, `Cookie`, `Host`, `If-*`, `Range`, `Referer`, `Upgrade`, `User-Agent`. For language/cookie/format use `Vary: Accept-Language`/`Vary: Cookie` or the specific `Netlify-Vary` instruction.

**Consistency rule:** a URL must return the same `Netlify-Vary` on every response — the first cached response's instructions win and later ones are ignored. `Netlify-Vary` + standard `Vary` are both respected (use `Vary` for format/encoding, and to pass instructions to an upstream CDN like Cloudflare).

## Cache tags & opt-out

Tag responses for taggable purging:

```
Netlify-Cache-Tag: tag1,tag2,tag3
```

- `Netlify-Cache-Tag` (Netlify CDN) wins over `Cache-Tag` (passed downstream). Some providers strip `Cache-Tag` — set both when proxying through them.
- Constraints: case-insensitive, UTF-8 only, ≤1024 chars/tag, ≤500 tags/response.

Opt a response out of automatic atomic-deploy invalidation with `Netlify-Cache-ID` (comma-separated; auto-registered as cache tags for purging; separate 500-ID limit):

```
Netlify-Cache-ID: cms-proxy,product,image
```

After opting out, purge on-demand after relevant changes (e.g. redirect/proxy or function changes behind a `Netlify-Cache-ID`).

## On-demand invalidation (purge)

Purge from a **deployed function** with `purgeCache` (site ID is passed automatically):

```ts
import { purgeCache } from "@netlify/functions";

export default async () => {
  await purgeCache(); // no args = purge everything for the site
  return new Response("Purged!", { status: 202 });
};
```

Purge by tag, optionally targeting a deploy/subdomain:

```ts
import { purgeCache } from "@netlify/functions";

export default async (req: Request) => {
  const cacheTag = new URL(req.url).searchParams.get("tag");
  if (!cacheTag) return;
  await purgeCache({
    tags: [cacheTag],
    deployAlias: "deploy-preview-11",
    domain: "early-access.company.com",
  });
  return new Response("Purged!", { status: 202 });
};
```

**Ambient credentials only work inside a deployed function.** From CI, local scripts, or the build, pass `token` (a personal access token read from an env var — never hardcoded) and `siteID`.

**Lambda-compatible functions** use the legacy `module.exports.handler = async (event, context) => {…}` signature and must pass `context.clientContext.custom.purge_api_token`:

```ts
import { purgeCache } from "@netlify/functions";

module.exports.handler = async (event, context) => {
  const token = context.clientContext.custom.purge_api_token;
  await purgeCache({ tags: ["tag1", "tag2"], token });
  return { body: "Purged!", statusCode: 202 };
};
```

Direct API (from outside a function) — `POST https://api.netlify.com/api/v1/purge` with `Authorization: Bearer <personal_access_token>` and `Content-Type: application/json`:

```sh
curl -X POST \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer <personal_access_token>" \
  --data '{"site_slug": "mysitename", "cache_tags": ["news"], "deploy_alias": "deploy-preview-11", "domain": "early-access.company.com"}' \
  'https://api.netlify.com/api/v1/purge'
```

- Purge by site: `site_id` or `site_slug`. By tag: `cache_tags` + site. Omitting `cache_tags` purges the whole site; an **empty** `cache_tags` list purges NOTHING.
- Identifier mapping: in the UI (Project configuration > General > Project details), **Project ID** = `site_id`, **Project name** = `site_slug`. See https://docs.netlify.com/api-and-cli-guides/api-guides/get-started-with-api#get-site.
- **Rate limit:** each tag or site can be purged only twice per 5s — exceeding returns `429`.

## Cache API (`caches` global)

Programmatic read/write of HTTP responses from Functions/Edge Functions. Use for caching individual components of a route or arbitrary fetches, alongside header-based route caching.

**Scope rule:** `caches.open()` anywhere, but `match`/`put`/`delete` **only inside the request handler** — doing them at module/global scope throws.

```ts
import type { Config, Context } from "@netlify/functions";

const cache = await caches.open("my-cache"); // ok in global scope

export default async (req: Request, context: Context) => {
  const request = new Request("https://example.com/expensive-api");
  const cached = await cache.match(request);
  if (cached) return cached;

  const fresh = await fetch(request);
  if (fresh.ok) {
    cache.put(request, fresh.clone()).catch((error) => {
      console.error("Failed to add to the cache:", error);
    });
  }
  return fresh;
};

export const config: Config = { path: "/cache-api-example" };
```

`CacheStorage` subset:
- `caches.match(request)` → `Response` from any cache, or `undefined`.
- `caches.open(name)` → `Cache`. Distinct names fragment the cache and lower hit ratio — use few, meaningful names.

`Cache` methods (all require `caches.open()`):
- `cache.match(request)` → `Response` | `undefined`.
- `cache.put(request, response)` → adds a response.
- `cache.add(request)` / `cache.addAll(requests)` → fetch + store.
- `cache.delete(request)` → `true`.
- `keys()` is **not implemented** — no way to list contents.

Consistency: reads/writes strongly consistent; **deletes eventually consistent** (a deleted entry may still return briefly).

**Cannot cache:** partial responses (206), `Vary: *`, or non-`GET` methods. Responses need a cache-control header with `max-age`/`s-maxage` ≥ 1s, `public` (not `private`/`no-cache`/`no-store`), and a 2xx status — otherwise storage errors. For responses you don't control, rewrite headers with `fetchWithCache`.

**Limits per invocation:** 100 lookups, 20 insertions/deletions. Exceeding: further lookups return nothing; writes/deletes no-op. Limits are shared across edge functions in a request but separate between serverless and edge functions. Cache data is per-region (not replicated), auto-invalidated on redeploy and on `max-age`/`s-maxage` expiry.

## `@netlify/cache` module

Install to get helpers, time constants (`MINUTE`/`HOUR`/`DAY`), and a `caches` export for local dev:

```
npm install @netlify/cache
```

**Local-dev workaround:** the `caches` global isn't part of Node.js. Netlify provides it in its Functions/Edge runtimes (live and under `netlify dev`), but if you run your framework's own dev server the global is undefined and throws — import it instead:

```ts
import { caches } from "@netlify/cache";
const cache = await caches.open("my-cache");
```

Requires Netlify CLI 20.0.3+; nothing persists locally (lookups return nothing, writes/deletes don't mutate). No functional change from the global.

### `cacheHeaders(settings)` → header object

```ts
import { cacheHeaders, DAY } from "@netlify/cache";

const headers = {
  "x-custom-header": "some value",
  ...cacheHeaders({
    ttl: 2 * DAY,          // s-maxage
    swr: HOUR,             // stale-while-revalidate
    durable: true,
    tags: ["product", "sale"],
    overrideDeployRevalidation: ["tag"], // opt out of atomic-deploy invalidation
    vary: {
      cookie: ["ab_test_name", "ab_test_bucket"],
      query: ["item_id", "page"], // or true for all
      country: ["us", ["es", "pt"]], // nested = OR
      language: ["en"],
      header: ["Device-Type"],
    },
  }),
};
```

For only generic (non-Netlify) headers, use the `cdn-cache-control` npm module instead.

### `fetchWithCache(resource, options?, cacheSettings?)`

Drop-in `fetch` that returns a cached response or fetches, stores, and returns. `cacheSettings` override conflicting response headers; with `swr`, background revalidation is handled automatically.

```ts
import { fetchWithCache, DAY } from "@netlify/cache";

const response = await fetchWithCache("https://example.com/expensive-api", {
  ttl: 2 * DAY,
  tags: ["product", "sale"],
  vary: { cookie: ["ab_test_name"], query: ["item_id", "page"] },
});
```

### `getCacheStatus(response | headers | headerString)`

Returns `{ hit, caches: { durable: { hit, stale, stored, ttl }, edge: { hit, stale } } }`.

```ts
const { hit, edge, durable } = getCacheStatus(response);
```

### `needsRevalidation(response)` → boolean

Only needed when calling `cache.match`/`cache.put` directly (not with `fetchWithCache`+`swr`). True when a Cache-API response is stale within its SWR window — return it, then revalidate in `context.waitUntil` and `cache.put` the fresh copy:

```ts
if (cached) {
  if (needsRevalidation(cached)) {
    context.waitUntil(
      fetch(request).then((fresh) => {
        const response = new Response(fresh.body, {
          headers: { ...Object.fromEntries(fresh.headers), ...cacheHeaders({ ttl: MINUTE, swr: HOUR }) },
        });
        return cache.put(request, response);
      })
    );
  }
  return cached;
}
```

## Durable cache

Add `durable` (serverless only) so edge nodes lacking a local copy check the shared durable cache before invoking the function — fewer invocations, better cache-miss latency. Eventually consistent, so multiple regions may still invoke the function a few times per version. Co-located with the site's functions region. Works with `Netlify-Vary`, SWR, and on-demand invalidation. **Next.js:** Next Runtime 5.5.0+ uses the durable cache automatically.

## Debugging with `Cache-Status`

Netlify sets `Cache-Status` (RFC 9211) on all responses. Check it on a **deployed** URL. Look for values starting `"Netlify Edge"` or `"Netlify Durable"`:

- `"Netlify Edge"; fwd=miss` — nothing cached.
- `"Netlify Edge"; hit` — served from cache.
- `"Netlify Edge"; hit; fwd=stale` — stale served while revalidating (SWR).
- Durable stored on miss: `"Netlify Durable"; fwd=uri-miss; stored=true; ttl=3600`.
- Durable hit: `"Netlify Durable"; hit; ttl=1234`.

`ttl` negative = seconds since expiry. Each request may hit a different cache instance — without production traffic or `durable`, expect several empty caches before a hit; repeat requests to warm one.

<!-- Gaps: package/method inconsistency in @netlify/cache local-dev docs (caches import shown with cache.set, not the documented cache.put) resolved to cache.put per Cache API surface. -->

<!-- system: agent-context/caching/system.md — human-owned, merged by ctx-gen; edit system.md, not this section -->
# Netlify house rules (caching)

These are org conventions, not docs facts — merged into the rendered skill by
ctx-gen and never generated. Owned by the skills maintainer.

1. Only `GET` responses are cached by the CDN. `POST`/`PUT`/etc. are never
   cached regardless of headers — expose cacheable data on a `GET` route
   (put the inputs in the URL or query string).
2. Without `Netlify-Vary: query=...`, the full query string is the cache key —
   every distinct query string (`utm_*`, `fbclid`, ...) is a separate cache
   entry. Enumerate only the params that actually change the response.
3. `netlify dev` does not emulate the CDN cache — a cache miss every time
   locally is expected, not a bug. Verify caching behavior on a deployed URL
   (Deploy Preview or production) via its `Cache-Status` header.
4. `purgeCache()` has ambient credentials only inside a deployed function.
   From CI, local scripts, or the build, pass `token` (a personal access
   token read from an env var, never hardcoded) and `siteID`.

From netlify/context-and-tools at b5894c0, shown as plain text under its license: MIT (permissive). The license text is in agent-plugin/LICENSE.

View SKILL.md on GitHub at b5894c0

Files

1 file, 15 KB in total. Each file has a sha256 in the pin's manifest; the first 12 characters are shown.

Skill folder on GitHub at b5894c0

Also in

The same SKILL.md content appears in these places too. We list it once and credit the oldest public source.

All 17 skills from Netlify

More from Netlify

  • test-skill

    This skill computes an AXIS Result by weighting three metrics: task completion at 60 percent, efficiency at 30 percent, and resilience at 10 percent.

    Claude Code, Codex, Cursor +4 · about 120 tokens · pinned 6 days ago

  • using-axis

    This skill teaches an agent how to run AXIS, a synthetic testing framework for AI agents, and interpret its output.

    Claude Code, Codex, Cursor +4 · about 4.9K tokens · pinned 6 days ago

  • netlify-access-control

    This skill routes users to the correct Netlify site-protection layer for their access-control need and clarifies the three distinct authentication concepts…

    Claude Code, Codex, Cursor +4 · about 2.9K tokens · pinned 6 days ago

  • netlify-agent-runner

    Run AI agents remotely on Netlify infrastructure to automate development tasks against your site's repository.

    Claude Code, Codex, Cursor +4 · about 2.6K tokens · pinned 6 days ago

  • netlify-ai-gateway

    Calls OpenAI, Anthropic Claude, Google Gemini, TypeSafe, or OpenRouter-hosted models from Netlify Functions and Edge Functions using each provider's official…

    Claude Code, Codex, Cursor +4 · about 3.3K tokens · pinned 6 days ago

  • netlify-blobs

    Store and retrieve unstructured files, objects, and cached data on Netlify using the @netlify/blobs module from Functions, Edge Functions, or Build Plugins.

    Claude Code, Codex, Cursor +4 · about 2.9K tokens · pinned 6 days ago

All Cloud and DevOps skills
  • netlify-config

    This skill configures Netlify builds and deployments via netlify.toml, _redirects, and _headers files at the repository root.

    by NetlifyOfficial publisher

    Claude Code, Codex, Cursor +4 · about 3.9K tokens · pinned 6 days ago

  • netlify-deploy

    Deploy and manage web projects on Netlify through Git continuous deployment, the CLI, Deploy Previews, and branch deploys.

    by NetlifyOfficial publisher

    Claude Code, Codex, Cursor +4 · about 3.3K tokens · pinned 6 days ago

  • netlify-frameworks

    Deploy and configure web frameworks on Netlify with build settings, SSR adapters, and environment variable management.

    by NetlifyOfficial publisher

    Claude Code, Codex, Cursor +4 · about 3.5K tokens · pinned 6 days ago

  • n8n-cli

    This skill enables agents to manage n8n workflows, credentials, executions, and instance resources via the n8n CLI command-line tool.

    by n8nOfficial publisher

    Claude Code, Codex, Cursor +4 · about 2.9K tokens · pinned 6 days ago

  • managing-streamlit-apps

    Manage Streamlit apps running in PostHog sandboxes through the full lifecycle: create, set or edit source code, start and stop the sandbox, poll status, and…

    by PostHogOfficial publisher

    Claude Code, Codex, Cursor +4 · about 1.4K tokens · pinned 6 days ago

  • release-candidate-prep

    Prepares a local Python SDK release candidate in a dedicated Git worktree when explicitly invoked with a version number.

    by OpenAIOfficial publisher

    Contains scripts

    Claude Code, Codex, Cursor +3 · about 4.8K tokens · pinned 6 days ago

The skills listed here are third-party software, collected from public GitHub repositories as a catalog of recent skills and provided as is. Agent Native does not write or control them and is not responsible for any harm, loss or damage caused by installing or using them. Labels and checks describe what we saw at one commit and are not a guarantee. Read a skill's files before you run it. Terms of use