A CORS error behind a CDN is almost never a browser bug and almost never an origin bug. It is a cache key problem. The origin returns the correct Access-Control-Allow-Origin for whoever asked first, the edge stores that response without Vary: Origin, and every later visitor from a different origin receives a header addressed to somebody else. Add the Fetch standard's 5-second default preflight lifetime when Access-Control-Max-Age is absent, and you get failures that appear on roughly one request in ten and vanish the instant someone bypasses the edge to test the origin directly.
Three failure classes account for nearly everything reported as a "CDN CORS problem". They have different symptoms and different fixes, and conflating them is why teams spend a week adding wildcards that make things worse.
Most CDNs strip or ignore request headers that are not part of the cache key, because forwarding everything destroys cacheability. If the origin computes Access-Control-Allow-Origin by reflecting the incoming Origin, and the Origin header never arrives, the origin either omits the header entirely or returns a null value. The browser then blocks the response with no useful diagnostic beyond the generic console message.
This is the dangerous one. The origin reflects the Origin header correctly, the edge caches the full response including the reflected Access-Control-Allow-Origin, and no Vary: Origin is present. Now one cached object serves an allow-origin value belonging to a single tenant. Tenant A works, tenants B through Z get a CORS error behind a CDN edge that is doing exactly what it was told. Worse, if the response is credentialed, you have leaked a cross-tenant response into a shared cache entry.
A preflight is an OPTIONS request carrying Access-Control-Request-Method and Access-Control-Request-Headers. Two opposite misconfigurations are common: the edge answers OPTIONS itself with a canned response that omits the headers your actual request needs, or the edge treats OPTIONS as uncacheable and forwards every single one. Any custom header — an Authorization variant, an X-Request-Id, or a Range header on video — pushes the request out of the safelisted set and forces that round trip.
Vary: Origin and Access-Control-Max-Age control two independent caches, and engineers routinely tune one while the other dominates the observed behaviour. Vary governs the shared cache at the edge and the private cache in the browser. Access-Control-Max-Age governs only the browser's preflight result cache, which is keyed per requesting origin, URL and credentials mode.
Browsers clamp the value you send. Chromium enforces a ceiling of 7200 seconds on Access-Control-Allow-Max-Age handling, Firefox permits up to 86400 seconds, and when the header is missing entirely the Fetch standard's default preflight lifetime is 5 seconds. As of 2026 that default is still the single largest source of unexplained OPTIONS traffic on public APIs: a single-page app firing 12 cross-origin calls with a custom auth header over an 80 ms round trip pays roughly 960 ms of preflight latency on cold start, and pays it again every 5 seconds of idle time.
Setting Access-Control-Max-Age to 7200 collapses that to one preflight per browser, per path, per two hours. It also means a header allowlist change takes up to two hours to propagate to active sessions. That is the trade, and it is not free.
Order matters. Apply these in sequence, verifying after each step rather than shipping all five and guessing which one helped.
| Edge configuration | Cache entries per URL | CORS correctness | Best for |
|---|---|---|---|
| Static allow-origin asterisk at the edge, Origin not forwarded | 1 | Correct only without credentials | Fonts, media segments, public assets |
| Origin forwarded, response cached without Vary | 1 | Broken and potentially leaky | Nothing |
| Origin forwarded, Vary: Origin honoured and keyed | One per distinct origin seen | Correct | Small, fixed set of client origins |
| Origin normalized to allowlist buckets before keying | One per bucket | Correct, fragmentation bounded | Multi-tenant APIs and white-label domains |
Normalizing the Origin header into a bounded set of allowlist buckets is the only option that keeps CORS correct without letting cache cardinality grow with the number of client domains.
Vary: Origin multiplies cache entries. A media API serving 2,000 objects to 40 tenant subdomains stores up to 80,000 variants instead of 2,000, and each edge pays a cold fill per tenant. On a workload with modest per-tenant traffic that is a measurable hit-ratio loss — plan on several percentage points, and measure it rather than assuming.
Negative caching is the sharpest edge. If the origin rejects an unknown Origin with a 403 and the edge caches 403 responses for 60 seconds without Vary, one bad requester poisons the object for every legitimate client in that region. Either exclude error status codes from caching on CORS-sensitive paths or ensure Vary is applied to them too.
Range requests on video deserve their own note: Range is not a safelisted request header, so byte-range playback triggers preflights, and 206 responses must carry the same allow-origin headers as the 200. Miss Access-Control-Expose-Headers for Content-Range and players silently fall back to full-file downloads.
Observability is the real gap. Browser consoles report a CORS failure without telling you which header was missing, edge logs rarely record response headers by default, and the failing request often never reaches the origin at all. Log Origin, the Vary value and the cache status on every CORS-eligible path before you start debugging. Platforms differ in how much of this is expressible: forwarding Origin, adding it to the cache key and rewriting response headers per path are standard edge header and cache-key configuration capabilities on BlazingCDN and most cost-focused CDNs, while some entry-level plans expose only a single static allow-origin toggle.
Because the CDN introduces a shared cache between the browser and the origin. The origin computes Access-Control-Allow-Origin per requester, but the edge stores one response for many requesters. Without Vary: Origin and Origin in the cache key, the first requester's header is replayed to everyone else, producing intermittent failures that disappear when the edge is bypassed.
Set Vary: Origin on any response whose Access-Control-Allow-Origin is computed by reflecting the request. Omit it when you return a static wildcard, since there is nothing to vary on and the header only fragments caches. Error responses on the same path need identical treatment, otherwise cached 403s become cross-tenant failures.
It reduces preflight volume, not asset load. Access-Control-Max-Age controls only the browser's preflight cache. Raising it from the 5-second Fetch default to 7200 seconds removes almost all repeat OPTIONS traffic for active sessions. The cost is propagation delay: changes to allowed methods or headers take up to that window to reach clients.
No. When Access-Control-Allow-Credentials is true, the browser rejects a wildcard Access-Control-Allow-Origin outright. The server must echo the exact Origin value after validating it against an allowlist, and that response must carry Vary: Origin so shared caches keep the variants separate.
Pick your three highest-traffic cross-origin paths and pull 24 hours of edge logs for them. Record four fields: Origin, cache status, response Access-Control-Allow-Origin and status code. If any URL shows more than one distinct allow-origin value against the same cache key, you already have a correctness bug that users are hitting silently. Then measure OPTIONS as a share of origin requests before and after setting Access-Control-Max-Age to 7200. Post the delta to your team channel — the number is usually larger than anyone expects.