Skip to main content
CDN

CDN troubleshooting

Fix caching, static file, compression, header, and routing problems on Easel's CDN.

Start with the response headers, and test with curl rather than a browser so the browser's own cache doesn't hide the result:

curl -I https://example.com/path

X-Easel-Cache tells you what the CDN did, and X-Easel-Id finds the request in Requests. Easel removes CDN-Cache-Control before the response reaches you, so check your code for the policy it sent. See Caching.

You seeWhat happened
HIT or STALEServed from the CDN cache; no function ran
MISSNot in cache; the deployment or your function produced it
BYPASSNot eligible for caching
3xx with LocationA redirect matched before any route ran
No function logCached, static, redirected, or blocked before your code

Other problems have their own pages:

ProblemPage
Certificate warnings, HTTPS not active, DNSDomain troubleshooting
Blocked or challenged requestsSecurity troubleshooting
Builds, missing environment variables, works locally but not deployedDeployment troubleshooting
Slow responses, tracing a failing requestTroubleshooting

Responses aren't cached

X-Easel-Cache is BYPASS

The request or response failed an eligibility check. Look for, in order of how often they cause it:

  • A Set-Cookie header on the response.
  • private, no-store, or no-cache, or no positive lifetime.
  • An Authorization or Range request header.
  • A method other than GET or HEAD, or an uncacheable status.
  • A higher-precedence header with a stricter policy. A Vercel-CDN-Cache-Control: no-store wins over CDN-Cache-Control.

X-Easel-Cache is always MISS

The response is eligible but no entry matches. Check that repeated requests use the same hostname, path, and query string, and that the lifetime is longer than the time between requests. A new deployment or a purge also starts fresh. Responses above the cacheable size are never stored; see Limits.

Don't add a cache-busting query string while debugging. It creates a new cache key and hides the original problem.

A function runs when you expected a hit

Beyond the checks above, the framework may render the route dynamically. In Next.js, reading cookies or headers can opt a route out of static rendering. Middleware that changes the response or rewrites to a different path also changes what's cached. See the framework guides.

Too many cache variants

Look for tracking parameters and unique IDs in query strings. Each distinct URL is its own cache entry.

Content is stale

Old content after a purge or revalidation

Check that:

  • The tag you invalidated exactly matches the tag on the response.
  • You updated the data before invalidating. Otherwise the page regenerated from old data.
  • The rendered page and the data it reads share a tag. If only the CDN entry was invalidated, the page regenerates from a still-fresh Runtime Cache or framework data-cache entry.
  • You purged the right deployment. Production and preview caches are separate.
  • The browser isn't using its own copy. A long browser max-age outlasts any CDN purge.

expireTag seems to do nothing

The deployment must be storing values through getCache(), and the CDN responses must carry the same tag. See Runtime Cache.

Visitors see an old version

A service worker or a stable URL with a long browser lifetime can keep serving old files. If the HTML still references an old hashed asset, the HTML is stale, not the asset; give HTML a revalidating policy. See Static files.

Cached data is shared between users

Set Cache-Control: private, no-store on user-specific routes, then purge. Check for personalization from cookies or middleware on a route that has a shared-cache policy. The CDN cache splits entries only by encoding, not by other Vary headers, so a response that depends on a request header, such as Accept-Language, needs its own URL or no shared caching. Short lifetimes don't make private content safe to cache. See Caching.

Static files

A file returns 404

Check that the file is in the build output, the output and root directories are right, the capitalization matches (paths are case-sensitive), and the URL includes your framework's base path or asset prefix. Test the deployment URL directly to rule out a production domain pointing at a different deployment.

An asset request runs a function or returns HTML

A broad rewrite, SPA fallback, or middleware matcher is catching the asset path. Exclude asset directories from catch-all rules and matchers.

A file has the wrong content type

Easel sets Content-Type from the file extension. Serve files with their correct extension, and check for a custom header rule overriding it.

Compression is missing

Request with Accept-Encoding: br, gzip and look for Content-Encoding. Compression is skipped when the client doesn't advertise it, the body is very small, the type is already compressed or not text, the response already has Content-Encoding, or it's a range request. Some proxies and tools advertise only gzip.

A decoding error in the browser means the app set Content-Encoding on a body it didn't encode, or the response was compressed twice. See Compression.

Headers

A request header is missing in your code

Platform headers exist only on deployed requests that reach a function. Check the exact name in Headers and whether your framework passes the original request headers through.

A custom response header disappears

The name may be reserved by Easel or hop-by-hop, a framework may drop it, or it may be set on a redirect hop instead of the final response. Use your own prefix, not X-Easel-*, and check every hop with curl -IL.

Headers differ between preview and production

Previews add X-Robots-Tag, may sit behind deployment protection, and can use different environment variables.

Routing

A redirect is unexpected or loops

Follow the chain and read each Location:

curl -IL --max-redirs 10 https://example.com/path

Loops usually come from two layers enforcing the same rule: a trailing-slash redirect that disagrees with your framework setting, middleware redirecting into its own matcher, or a locale redirect adding and removing a segment. Domain-level loops, such as apex and www pointing at each other, are covered in Domain troubleshooting.

An external rewrite fails

Test the origin directly with curl -I, then through Easel, and compare status and Location. Check that the origin resolves publicly, has a valid certificate, accepts requests from Easel, gets any auth headers it needs, and doesn't redirect back to your Easel URL.

An image returns an error or isn't optimized

Check your framework guide, that remote sources are in the framework's allowlist (such as images.remotePatterns in Next.js), that the requested width is one your configuration allows, and the source image's response headers. See Image optimization.

Contact support

Include the full URL, the time with time zone, X-Easel-Id, X-Easel-Cache, the status, whether preview, production, or both are affected, and whether a proxy sits in front of Easel. Don't send cookies, tokens, or API keys.