| name | cache |
| description | Audit and improve deco store caching across all three layers: loader cache, stale edge cache (async render), and HTML page cache. Use when the user asks to improve cache, reduce latency, optimize loaders, configure TTL/cache keys, enable HTML cache, or debug cache-control headers on a deco storefront. |
Deco Cache
Deco has three caching layers, each operating at a different level. Apply all three together for lowest possible latency.
Layer 1 — Loader Cache (server-side, per loader)
Caches the data fetched by a loader before the section is rendered.
Sync loaders do not need cache. A sync loader runs entirely in memory with no I/O — it is already instant. Adding cache to it only introduces overhead (key computation, serialization, cache lookup) and makes the code harder to reason about. Cache applies exclusively to async loaders that perform network calls or other I/O.
Configuration
cache and cacheKey must be exported from a dedicated loader file inside the loaders/ folder.
They never work when defined inline inside a section file. If the loader is written directly inside the section, extract it to loaders/ first, then add the cache config there.
export const cache =
export const cacheKey = (props, req, ctx) => string | null
export const loader = ...
export const cache = "stale-while-revalidate";
Cache modes
| Mode | Behavior | When to use |
|---|
"no-store" (default) | Disables cache; also prevents CDN section caching | Carts, sessions, user-specific data |
"no-cache" | Always fetches fresh, but section can still be CDN-cached | Loader must be fresh but section is safe to cache |
"stale-while-revalidate" | Returns cached data, revalidates in background (default TTL: 60s) | Best default for public, read-mostly loaders |
{ maxAge: number } | SWR behavior with a custom TTL in seconds | Public data that is stable for longer periods (e.g. { maxAge: 60 * 60 } for 1 hour) |
Default TTL: 60 seconds (override via CACHE_MAX_AGE_S env var or per-loader { maxAge }).
Note: export const maxAge = N as a separate export is not a supported API and will be silently ignored. Always use export const cache = { maxAge: N } to set a custom TTL.
Cache key rules
The final key is composed of: resolver name + return value of cacheKey.
- Never use the raw
req.url or url.href as the key. Real URLs carry tracking params (utm_*, gclid, fbclid, session tokens, etc.) that make every request look unique, effectively disabling the cache.
- Build the key from props, not from the URL. Use the loader
props as the primary source of truth — they already represent the canonical inputs the framework parsed.
- If you do use the URL, reconstruct it with only the params you need (see examples below).
- Include segmentation traits (locale, currency, segment token) when they affect the result.
- Return
null to disable caching for a specific invocation (e.g. logged-in user).
Examples
export const cache = "stale-while-revalidate";
export const cacheKey = (props: { slug: string }) => props.slug;
export const cache = "stale-while-revalidate";
export const cacheKey = (_props: unknown, _req: Request, ctx: AppContext) => {
if (!ctx.isAnonymous) return null;
return ctx.segment?.token ?? "anonymous";
};
export const cache = "stale-while-revalidate";
export const cacheKey = (props: { category: string; page: number }) =>
`${props.category}:${props.page}`;
export const cache = { maxAge: 60 * 60 };
export const cacheKey = (props: { category: string }) => props.category;
export const cache = "stale-while-revalidate";
export const cacheKey = (props: { slug: string }, req: Request) => {
const { origin, pathname } = new URL(req.url);
return `${origin}${pathname}?slug=${props.slug}`;
};
export const cache = "no-store";
Invalidation
Automatically invalidated on every new deployment. No manual invalidation before TTL expiry.
Layer 2 — Stale Edge Cache (CDN, per section)
Caches the fully rendered section HTML at the CDN edge. Sections load asynchronously; those that exceed the render time budget are served as skeletons on first load, then fetched and patched from the CDN.
Default TTL headers
| Directive | Value | Meaning |
|---|
s-maxage | 60s | CDN holds the section for 60 seconds |
stale-while-revalidate | 3600s | Serve stale for up to 1 hour while revalidating |
stale-if-error | 86400s | Serve stale for up to 24 hours on origin errors |
Cacheability rule
A section is cached at the CDN only if all of its loaders are configured for caching (i.e. none use "no-store"). A single uncached loader makes the whole section uncacheable.
Async rendering
- Enabled by default for all sections.
- To disable: turn off Optimization in section properties in the Admin.
- Off-screen sections are deferred until the user scrolls near them.
Layer 3 — HTML Page Cache (CDN, full page)
Caches the fully assembled page HTML at the CDN edge. A cache hit means zero server involvement.
Minimum versions required
| Package | Minimum version |
|---|
deco | 1.199.0 |
apps (VTEX sites) | 0.153.0 |
How it works
This layer is available for VTEX sites only. When the site is on the required versions, the deco runtime automatically emits Cache-Control: public on cacheable HTML responses. The CDN (Cloudflare) is already configured globally to cache responses that carry this header — no per-site CDN changes are needed.
Default Cache-Control for cached pages:
public, max-age=90, s-maxage=90, stale-while-revalidate=3600, stale-if-error=86400
Anonymous requests from logged-out users with no active promotions or price tables are cached. Everything else (logged-in users, personalized segments) receives no-store.
How to enable
Update deps in deno.json:
{
"imports": {
"deco/": "https://denopkg.com/deco-cx/deco@1.199.0/",
"apps/": "https://denopkg.com/deco-cx/apps@0.153.0/"
}
}
No CDN configuration, no env vars needed. DECO_PAGE_CACHE_CONTROL is still supported to override the default Cache-Control value if needed.
Verify
curl -sI https://www.site.com.br/ | grep -i cache-control
curl -sI https://www.site.com.br/ \
-H "Cookie: VtexIdclientAutCookie=somevalue" \
| grep -i cache-control
Common issues
| Symptom | Likely cause |
|---|
No Cache-Control: public on anonymous requests | apps below 0.153.0; active segment (campaigns/priceTables/regionId); or Set-Cookie present in the response (any loader or middleware setting a cookie forces the runtime to skip caching) |
| Cloudflare still serving MISS | Response missing Cache-Control: public |
⚠️ Do NOT use a site-level _middleware.ts to strip cookies
Adding a routes/_middleware.ts that strips cookies and overrides no-store is dangerous: it can re-enable caching for responses intentionally marked non-cacheable (e.g. users with non-default price tables), serving wrong prices or promotions to other users. If a site already has such a middleware, remove it.
How the layers interact
Request
└─ HTML Page Cache (CDN) ──── HIT → serve page, done
└─ MISS
└─ Stale Edge Cache (CDN, per section) ── HIT → serve section HTML
└─ MISS
└─ Loader Cache (server) ── HIT → serve cached data, render
└─ MISS → fetch from upstream API, render, populate caches
Agent workflow — improving cache on a store
When auditing or improving a store's cache:
- Find all async loaders — look for files in the
loaders/ folder exporting a loader function that performs I/O (API calls, fetches). Skip sync loaders that only transform props — caching them is actively harmful. Also skip any loader defined inline inside a section file — cache config does not work there and the loader must be extracted to loaders/ before cache can be applied.
- Check for missing cache config — any async loader without
export const cache defaults to "no-store" and blocks CDN section caching.
- Classify each loader:
- Public / read-mostly →
"stale-while-revalidate" or { maxAge } + a stable cacheKey
- Must-be-fresh but section is safe →
"no-cache" + cacheKey
- User-specific / session →
"no-store" (no cacheKey needed)
- Write
cacheKey from props, not from the URL. The URL contains tracking params and query strings that vary per visitor and destroy cache hit rates. Build the key by composing only the props fields that affect the result. Return null for authenticated contexts.
- Verify CDN cacheability — after configuring loaders, check that sections whose loaders are all cached are actually being served from the edge.
- Check HTML page cache eligibility — read
deno.json and confirm deco ≥ 1.199.0 and apps ≥ 0.153.0. If not, update deps. Then verify with curl -sI https://www.site.com.br/ | grep -i cache-control.
Priority order
- Add
cache + cacheKey to every public async loader missing them.
- Fix loaders using
"no-store" unnecessarily (blocking section CDN caching).
- Audit existing
cacheKey implementations — replace any that use req.url or url.href directly with prop-based keys.
- Tune TTLs: shorter for volatile data, longer (5–30 min) for stable catalog data.
- Enable HTML page cache: check
deno.json for deco@1.199.0 + apps@0.153.0 and update if needed.