Bypassing CDN cache for authenticated CMS users

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:

flowchart TD
  Req["Incoming request at edge"] --> Auth{"Auth session cookie present?"}
  Auth -->|yes| Bypass["Set no-store / private, skip cache lookup"]
  Bypass --> Origin["Origin: fresh, session-scoped payload"]
  Auth -->|no| Strip["Strip cookies for cache hit ratio"]
  Strip --> Lookup{"Edge cache hit?"}
  Lookup -->|hit| Served["Serve cached public copy"]
  Lookup -->|miss| OriginPub["Origin: public payload, then cache"]

Cloudflare Workers intercept and mutate request headers via standard Web APIs:

TypeScript
// 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:

VCL
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:

TypeScript
// 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.

Conclusion

Enforce Vary, evaluate cookies deterministically at the edge, and align middleware with origin cache policy: editors get accurate draft previews, the public gets cached delivery, and no session leaks between them.