Skip to main content
CDN

CDN troubleshooting

Diagnose caching, redirects, compression, HTTPS, and routing issues on Easel CDN.

Use this guide when a request is not being cached, stale content remains visible, compression is missing, HTTPS is failing, or the response does not follow the expected route.

Start by inspecting the response headers:

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

Useful headers include:

X-Easel-Id: req_...
X-Easel-Cache: HIT
Cache-Control: public, max-age=0, must-revalidate
CDN-Cache-Control: public, s-maxage=300
Content-Encoding: br

Age may also appear on some responses. Treat X-Easel-Cache as the authoritative cache-result header. Absence of Age is not an error.

Run the same request more than once when testing cache behavior:

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

A cacheable response commonly returns MISS on the first request and HIT on a later request.

Identify the request path

The fastest way to diagnose a CDN issue is to determine how Easel handled the request.

ObservationLikely path
Firewall or challenge responseSecurity rules completed the request
Redirect responseA routing or application redirect matched
X-Easel-Cache: HITA fresh cached response was served
X-Easel-Cache: STALEA stale cached response was served while refreshing in the background
X-Easel-Cache: MISSEasel retrieved or generated a new response
X-Easel-Cache: BYPASSThe request or response was not cacheable
No function logThe request may have been cached, static, redirected, or blocked
Function log presentApplication compute handled the request

Use X-Easel-Id to correlate the client response with Easel logs and traces.

The response is not cached

X-Easel-Cache is BYPASS

BYPASS means Easel did not attempt to store or reuse the response.

Check for these common causes:

  • The request method is not GET or HEAD.
  • The request contains Authorization.
  • The request contains Range.
  • The response contains Set-Cookie.
  • The response is marked private.
  • The response contains no-store or no-cache.
  • The response does not include a positive shared-cache lifetime.
  • The response status is not cacheable.

Inspect the relevant headers:

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

A basic cacheable policy looks like:

Cache-Control: public, max-age=0, must-revalidate
CDN-Cache-Control: public, s-maxage=300

If the response includes:

Set-Cookie: session=...

Easel bypasses shared caching.

Separate cookie-setting behavior from cacheable content when possible.

X-Easel-Cache is always MISS

A repeated MISS means the request is eligible for cache processing, but Easel is not finding a reusable matching entry.

Check that repeated requests use the same:

  • hostname
  • path
  • query string
  • deployment
  • request method
  • supported Vary values

These URLs may create different cache entries:

/products
/products?page=1
/products?page=2

Also check whether:

  • the TTL is too short
  • the response changes its Vary header
  • a deployment occurs between requests
  • the route is being purged repeatedly
  • the framework marks the route as dynamic
  • the response exceeds a platform cache limit

The second request still returns MISS

Wait long enough for the first response to complete, then repeat the exact request.

Use:

curl -sS -D - -o /dev/null https://example.com/path
curl -sS -D - -o /dev/null https://example.com/path

Confirm that the first response actually includes a positive CDN lifetime.

For example:

CDN-Cache-Control: public, s-maxage=300

A browser extension, authentication layer, cookie, or upstream proxy may also alter otherwise identical requests.

The response has Cache-Control, but is not cached

Easel uses cache headers in this order:

  1. Vercel-CDN-Cache-Control
  2. CDN-Cache-Control
  3. Cache-Control

A higher-priority header may override the policy you are inspecting.

For example:

Vercel-CDN-Cache-Control: no-store
CDN-Cache-Control: public, s-maxage=3600

The response is not cached because the Vercel-compatible header takes precedence.

Inspect all three headers together.

Cached content is stale

Content remains old after a purge

Check that:

  • the purge targeted the correct project
  • the correct environment was selected
  • the tag or URL exactly matches the cached entry
  • the browser is not serving its own cached copy
  • the underlying application data was updated before the purge
  • another cache layer still contains old data
  • the route regenerated successfully after invalidation

A CDN purge cannot remove a response already stored in the visitor’s browser.

Compare:

Cache-Control: public, max-age=3600
CDN-Cache-Control: public, s-maxage=86400

The browser may reuse its copy for one hour even after Easel’s shared cache is purged.

For content that must reflect CDN invalidation quickly, use:

Cache-Control: public, max-age=0, must-revalidate
CDN-Cache-Control: public, s-maxage=86400

A page regenerates with old data

The CDN response may have been invalidated while the application continued using stale Runtime Cache or framework data-cache entries.

Use a shared cache tag across:

  • the rendered HTTP response
  • Runtime Cache values
  • framework data-cache entries
  • related routes

Alternatively, invalidate each layer explicitly.

Update the source of truth before initiating invalidation:

1. Update the database or content source
2. Confirm the update succeeded
3. Purge the relevant tag or route
4. Allow the next request to regenerate content

Purging before the update completes can cause the route to regenerate using old data.

A purge affects less content than expected

A URL purge targets one cache key. It does not automatically invalidate related URLs.

Purging:

/products/123

does not necessarily purge:

/products
/api/products/123
/products/123?currency=USD

Use a shared tag such as:

Cache-Tag: products product:123

when one data change affects multiple representations.

Browser caching differs from CDN caching

The browser and Easel can use different cache lifetimes.

Cache-Control: public, max-age=60
CDN-Cache-Control: public, s-maxage=3600

This means:

  • the browser may reuse its copy for 60 seconds
  • Easel may reuse its copy for one hour

When debugging, test with curl to avoid relying on the browser’s local cache.

You can also disable the browser cache temporarily in browser developer tools.

Do not use a cache-busting query parameter as the first diagnostic step. A changed query string may create a different CDN cache key and hide the original problem.

A static file returns 404

Check that:

  • the file exists in the framework’s public or output directory
  • the filename uses the correct letter casing
  • the build includes the file
  • the route does not conflict with a rewrite or function
  • the configured root directory is correct
  • the expected deployment is receiving the request

Production filesystems are case-sensitive.

For example:

/logo.svg
/Logo.svg

may refer to different files.

Inspect the build output and preview deployment before promoting the change to production.

An old static asset is still being served

Confirm that the page references the current asset URL.

Content-hashed files should receive a new filename when their contents change:

/assets/app.9f3ac21.js
/assets/app.32bca78.js

If HTML still references the old asset, the problem may be stale HTML rather than stale asset storage.

Check:

  • the HTML cache policy
  • deployment promotion status
  • service-worker caches
  • browser caches
  • hardcoded asset URLs
  • upstream proxies

A service worker can continue serving old files independently of Easel’s CDN cache.

A redirect is unexpected

Inspect the response without following redirects:

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

Look for:

HTTP/2 301
Location: https://example.com/other-path

Possible redirect sources include:

  • HTTP-to-HTTPS enforcement
  • project routing rules
  • framework configuration
  • middleware
  • application code
  • canonical-domain redirects
  • an upstream proxy

Follow the complete redirect chain with:

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

Review each Location header to identify where the chain begins.

The site is stuck in a redirect loop

Redirect loops commonly happen when two layers independently enforce the same rule.

Check for combinations such as:

  • Easel and an upstream proxy both forcing HTTPS
  • apex and www redirects pointing at each other
  • middleware redirecting back to its own matcher
  • application code disagreeing with forwarded protocol headers
  • locale redirects repeatedly adding or removing a path segment

Use:

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

Inspect each hop.

When a proxy sits in front of Easel, ensure that it connects to Easel over HTTPS and preserves the original protocol correctly.

A firewall blocks a valid request

Use the returned request ID and any firewall diagnostic headers to inspect the matching security event.

Possible causes include:

  • a custom WAF rule
  • managed-rule detection
  • an IP or network restriction
  • rate limiting
  • Attack Mode
  • bot or abuse detection
  • an upstream proxy causing many visitors to share one apparent IP

Reproduce the request with the same:

  • URL
  • method
  • headers
  • body
  • authentication state

Do not disable broad security protections permanently to resolve one false positive. Narrow the matching rule or add a scoped exception.

See Security.

A function was not invoked

The request may have completed before reaching application compute.

Common causes include:

  • a fresh CDN cache hit
  • a static deployment asset
  • a redirect
  • a firewall block
  • a challenge response
  • middleware returning a response
  • the request reaching a different route than expected

Check:

X-Easel-Cache
X-Easel-Id

Then inspect project request logs rather than only function logs.

A missing function invocation does not necessarily indicate that Easel dropped the request.

A function runs when a cache hit was expected

Check that:

  • the response includes a positive CDN TTL
  • the route does not set cookies
  • the request does not include authentication
  • query strings are identical
  • Vary values are stable
  • no purge occurs between requests
  • the framework has not opted the route into dynamic rendering
  • middleware does not alter the cache key or response
  • the response status is cacheable

For Next.js, request-time APIs such as cookies or headers can make a route dynamic depending on the framework version and route configuration.

See the relevant framework guide.

Compression is missing

Inspect the request and response:

curl -I \
  -H 'Accept-Encoding: br, gzip' \
  https://example.com/app.js

Look for:

Content-Encoding: br
Vary: Accept-Encoding

Compression may not be applied when:

  • the client does not advertise Brotli or gzip
  • the response type is not compressible
  • the response is below the minimum size
  • the response is already encoded
  • compression would not reduce the response size
  • the request is a range request
  • the response does not include a recognized Content-Type
  • streaming behavior prevents buffering or transformation

Compare the compressed and uncompressed response sizes:

curl -sS \
  -H 'Accept-Encoding: identity' \
  -o /dev/null \
  -w '%{size_download}\n' \
  https://example.com/app.js

curl -sS \
  --compressed \
  -o /dev/null \
  -w '%{size_download}\n' \
  https://example.com/app.js

See Compression.

HTTPS is not active

Check the custom-domain status in Easel.

Common causes include:

  • missing DNS records
  • conflicting DNS records
  • DNS changes that have not propagated
  • a restrictive CAA record
  • a proxied DNS record
  • DNSSEC errors
  • the domain pointing to another project
  • certificate issuance rate limits

Verify DNS independently:

dig example.com
dig CNAME www.example.com
dig CAA example.com

Use the exact DNS values displayed in Easel.

Do not repeatedly remove and re-add the domain while DNS is still propagating.

The browser shows a certificate warning

Confirm that:

  • the exact hostname is configured
  • certificate provisioning is complete
  • DNS points to the correct Easel project
  • an upstream proxy is not serving another certificate
  • the device clock is correct
  • local security software is not intercepting TLS

Inspect the certificate:

openssl s_client \
  -connect example.com:443 \
  -servername example.com

A certificate for example.com does not automatically cover every subdomain.

See HTTPS and TLS.

HTTP does not redirect to HTTPS

Check:

curl -I http://example.com

The response should redirect to the HTTPS URL.

If it does not, confirm that:

  • the hostname is active
  • the request reaches Easel
  • an upstream proxy is not intercepting HTTP
  • no conflicting routing rule handles the request first

A custom response header is missing

The header may have been:

  • overwritten by a higher-priority routing rule
  • removed by framework behavior
  • replaced by Easel because it uses a reserved name
  • treated as hop-by-hop
  • removed by an upstream proxy
  • rejected because of a size limit
  • added only to a route that did not handle the request

Avoid custom headers in the reserved X-Easel-* namespace.

Use:

X-My-App-Version: 42

instead of:

X-Easel-App-Version: 42

Inspect the complete redirect chain because headers on an intermediate response do not necessarily appear on the final destination.

A request metadata header is missing

Some headers are available only on deployed requests and do not appear during local development.

Also check whether:

  • the route reached application compute
  • the framework exposes the original request headers
  • an upstream proxy changed the request
  • the header is supported for that route type
  • the documented header name is correct

Do not depend on undocumented internal headers.

See Request and response headers.

Client IP or country appears incorrect

IP geolocation is approximate.

Unexpected values may also occur when:

  • a VPN is in use
  • a corporate gateway sends the request
  • a mobile carrier proxies traffic
  • another CDN or reverse proxy sits in front of Easel
  • the application reads an untrusted forwarded header
  • the IP database has not yet reflected a recent allocation

Use the documented trusted Easel header rather than arbitrary client-supplied X-Forwarded-For values.

Do not use IP geolocation as the only signal for high-impact access or compliance decisions.

Preview and production behave differently

Check for differences in:

  • environment variables
  • custom domains
  • root directories
  • build commands
  • framework versions
  • routing rules
  • firewall policies
  • function placement relative to databases
  • cache state
  • preview access protection
  • database branches or external services

Preview and production deployments have separate URLs and deployment contexts. A warm production cache does not imply that the preview cache is also warm.

Reproduce the issue using the preview deployment’s exact hostname.

A change works locally but not after deployment

Production builds can expose differences involving:

  • case-sensitive paths
  • missing environment variables
  • build-time versus runtime variables
  • unsupported filesystem assumptions
  • native dependencies
  • framework adapter output
  • route generation
  • static versus dynamic rendering
  • Node.js versions
  • external network access

Inspect the build logs first, then use the request ID to inspect runtime logs.

Do not assume that a local development server has the same caching, routing, or security behavior as a deployed preview.

An external rewrite fails

Check that:

  • the destination URL uses the correct protocol
  • the origin hostname resolves publicly
  • the origin accepts requests from Easel
  • the origin certificate is valid
  • required authentication headers are present
  • the rewrite preserves the required path and query string
  • the origin is not redirecting back to the Easel URL
  • an upstream firewall is not rejecting Easel traffic

Test the origin directly where appropriate:

curl -I https://origin.example.net/path

Then test through Easel:

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

Compare the status, Location, cache, and request headers.

A response is unexpectedly personalized

A shared cached response should not contain visitor-specific data.

Immediately check for:

  • missing private or no-store
  • authentication performed without cache bypass
  • personalization based on cookies not represented in the cache key
  • unsupported or incorrect Vary
  • middleware adding user-specific content before caching
  • application state leaking between requests

User-specific responses should generally return:

Cache-Control: private, no-store

Do not rely on short TTLs to protect private content.

Purge the affected content and correct the cache policy before restoring the route.

A response is unexpectedly slow

Determine whether the request is:

  • a cache hit
  • a cache miss
  • a bypass
  • a static asset
  • a dynamic function
  • an external-origin request

Start with:

curl -sS -D - -o /dev/null \
  -w 'connect=%{time_connect} starttransfer=%{time_starttransfer} total=%{time_total}\n' \
  https://example.com/path

A slow cache hit points toward network conditions, response size, or connection setup.

A slow miss may involve:

  • function startup
  • application execution
  • database latency
  • external API latency
  • distance between the CDN edge and US East compute
  • response streaming
  • cache regeneration

Use Easel traces to separate platform time from application and downstream-service time.

Collecting information for support

Include the following when reporting a CDN issue:

  • project name or ID
  • deployment ID
  • affected hostname
  • complete URL
  • approximate request time and timezone
  • request method
  • response status
  • X-Easel-Id
  • X-Easel-Cache
  • relevant cache-control headers
  • whether the issue affects preview, production, or both
  • reproduction steps
  • whether an upstream proxy is present

Do not send:

  • session cookies
  • authorization tokens
  • private API keys
  • password-reset links
  • unredacted personal data

A reproducible request is especially useful:

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

Include safe request headers only when they are required to reproduce the behavior.