Bypassing CDN Cache for Authenticated CMS Users
Within Content Delivery Network Routing Logic, this guide handles authenticated traffic. A CDN keys its cache on URI, method, and query params — none of which capture a session cookie or Bearer token. So when an editor previewing a draft hits an edge node that lacks a bypass rule, they get the stale public copy a prior anonymous user generated. Bypassing the cache for authenticated CMS users comes down to precise header negotiation, deterministic edge routing, and strict origin hygiene.
Diagnosing Cache-Key Collisions
Session leakage almost always traces to a missing or misconfigured Vary directive: the edge can’t distinguish authenticated traffic from anonymous, so it serves the cached anonymous response — causing hydration errors, exposing drafts on public endpoints, and corrupting personalized state. Diagnose by inspecting X-Cache, CF-Cache-Status, or X-Served-By; a HIT on a request carrying valid session credentials confirms the failure.
Then check the origin. It must never emit Cache-Control: public on a session-dependent payload — that forces the edge to cache dynamic responses. Enforce Cache-Control: private, no-store, max-age=0 on authenticated endpoints, and use Vary to declare exactly which request headers vary the cache. The MDN HTTP caching reference is the authority on the semantics; any Data Fetching & Caching Strategies design has to account for both anonymous and authenticated lifecycles.
Edge-Level Bypass Patterns
Edge platforms run scriptable routing that evaluates session state before the request reaches origin. Pushing this Content Delivery Network Routing Logic to the edge sheds origin load while keeping anonymous cache hit ratios high.
How the edge splits authenticated and anonymous traffic:
Cloudflare Workers intercept and mutate request headers via standard Web APIs:
// worker.ts
export default {
async fetch(request: Request, env: Env, ctx: ExecutionContext) {
const cookieHeader = request.headers.get('Cookie') || '';
const hasAuthSession = /cms_session=|auth_token=/.test(cookieHeader);
if (hasAuthSession) {
const bypassHeaders = new Headers(request.headers);
bypassHeaders.set('Cache-Control', 'no-store, no-cache, must-revalidate');
bypassHeaders.set('Pragma', 'no-cache');
// Forward request to origin with explicit cache-bypass directives
return fetch(request, { headers: bypassHeaders });
}
// Anonymous traffic proceeds to standard edge cache lookup
return fetch(request);
}
};
The worker runs at the edge, matches the Cookie header against a compiled regex, and on a session hit rewrites the outgoing request with no-store so origin skips the edge cache. See the Cloudflare Workers docs for deployment.
Fastly VCL does the same in vcl_recv, using state-machine routing to bypass the cache-lookup phase:
sub vcl_recv {
# Match authenticated session cookies
if (req.http.Cookie ~ "cms_session=" || req.http.Cookie ~ "auth_token=") {
set req.http.X-Cache-Bypass = "true";
return(pass);
}
# Strip cookies from anonymous requests to maximize cache hit ratio
unset req.http.Cookie;
}
return(pass) routes straight to origin without a cache lookup. Unsetting req.http.Cookie on anonymous traffic stops tracking and analytics identifiers from fragmenting the cache. The Fastly VCL reference documents the exact cache-state execution order.
Framework Middleware Strategies
Next.js and Remix abstract edge routing into framework middleware. The bypass must run before the data-fetching layer initializes, or it serves a stale SSR/ISR payload. Next.js App Router middleware runs at the edge, so cookie evaluation costs nothing on static routes:
// middleware.ts
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';
export function middleware(request: NextRequest) {
const sessionCookie = request.cookies.get('cms_session');
const authToken = request.cookies.get('auth_token');
if (sessionCookie || authToken) {
const response = NextResponse.next();
// Instruct downstream CDNs and browsers to bypass cache
response.headers.set('Cache-Control', 'private, no-store, max-age=0');
response.headers.set('X-Auth-Route', 'true');
return response;
}
return NextResponse.next();
}
export const config = {
matcher: ['/api/cms/:path*', '/dashboard/:path*', '/preview/:path*'],
};
The matcher scopes evaluation to authenticated paths; on a session cookie, the middleware sets Cache-Control: private, no-store before any data fetch, so server components and API routes get fresh, session-scoped payloads while public routes keep ISR performance.
Client-Side State Alignment & Validation
The bypass has to align with client state or hydration mismatches return. With React Query or SWR, set staleTime and gcTime to respect the edge no-store. Apollo Client needs a custom link to attach the session token to every query. Stop client hydration from clobbering server-rendered authenticated state with ssr: false or deferred fetching on personalized components.
Validate against a staging CDN: inject session cookies with Playwright or Cypress and assert X-Cache returns MISS, DYNAMIC, or BYPASS, that draft endpoints return 200 with the right payload version, and that public endpoints keep their hit ratio. Contract-test that origin Cache-Control survives the edge transform — see Automated Testing for Headless Integrations.
Configuration Reference
| Setting | Value | Why |
|---|---|---|
| Session cookie names | cms_session, auth_token |
The only cookies that trigger bypass; everything else is stripped. |
| Cookie allow-list for anonymous traffic | consent cookie only | Tracking cookies must not reach the cache key. |
| Origin header for sessions | Cache-Control: private, no-store |
The origin’s defence if an edge rule is ever missing. |
Vary on authenticated paths |
Cookie |
Prevents any shared cache from merging session responses. |
| Preview hostname | separate, never cached | Structural isolation, independent of cookie rules. |
Stripping cookies from anonymous requests is what keeps the hit ratio high, but it needs an allow-list rather than a block-list. Marketing tools add new cookies regularly, and a block-list lets each new one fragment the cache until someone notices. Strip everything except the handful of cookies the application really reads.
Gotchas & Edge Cases
Vary: Cookiewithout stripping. Varying on the fullCookieheader gives every anonymous visitor with an analytics cookie a private cache entry, which destroys the hit ratio. Strip or normalize cookies first, then vary.- Logged-in users on public pages. Bypassing the cache for every page whenever a session exists can multiply origin load for sites with many signed-in readers. Bypass only routes that render session-specific content, and render personal fragments client-side on otherwise public pages.
- Service workers. A service worker can cache an authenticated response in the browser and serve it after sign-out. Exclude authenticated routes from service worker caching.
- Stale sessions after sign-out. Clearing the session cookie on sign-out is not enough if the edge cached a private response under a public key earlier. That is why the origin header matters: if the response was never cacheable, sign-out cannot expose it.
- Preview cookies on the public host. Draft-mode cookies from the framework should be scoped to the preview host or path, so an editor browsing the public site later is not silently served uncached drafts.
Worked Example
A membership publication showed members’ names in the header and found, after a CDN migration, that some anonymous visitors saw “Welcome back, Dana”. The new CDN configuration ignored a Vary: Cookie header the old CDN had honoured, and the origin sent Cache-Control: public, s-maxage=60 on all HTML. The fix had three parts: an edge rule that passes any request carrying the session cookie, private, no-store on every response rendered with a session, and moving the greeting to a client-side fragment so article pages could stay cacheable for members too. The hit ratio for anonymous traffic returned to its old level within a day, and the member greeting no longer depended on cache behaviour at all.
Rollout Checklist
- List every cookie the application reads and build the anonymous allow-list from it.
- Add the edge bypass rule for session cookies before the cache lookup.
- Send
private, no-storefrom the origin on every response rendered with a session. - Move per-user fragments such as greetings and carts to client-side or edge-side includes.
- Put preview on its own hostname with caching disabled at the CDN.
- Add a staging test that alternates authenticated and anonymous requests and checks for leaks.
Frequently Asked Questions
Can signed-in users still benefit from caching?
Yes, if personal content is separated from shared content. Serve the shared page from cache and load a small personalized fragment client-side or through an edge-side include. The page itself then never varies by session and stays cacheable for everyone.
Is checking the cookie at the edge enough on its own?
No. Edge rules can be misconfigured or bypassed during a migration, as in the example above. The origin must also mark session responses as private, no-store, so a missing edge rule results in a cache miss rather than a leak.
How do I test the bypass?
Request the same URL with and without a session cookie against staging, and assert the cache-status header: BYPASS, PASS or DYNAMIC with the cookie, HIT after warm-up without it. Then request anonymously after an authenticated request and assert the body contains no session-specific markers.
Does this apply to API routes as well as pages?
Yes. Authenticated API routes, such as a saved-articles endpoint or a comment draft endpoint, need the same private, no-store header and the same edge bypass. They are easier to overlook because they are fetched in the background.
What about CDN features that cache per user automatically?
Some platforms offer per-user or per-session caching modes. They can help for dashboards, but they multiply cache storage and still depend on correct session detection. For CMS sites, keeping authenticated responses uncached and personal fragments small is simpler and safer.