DEV Community

Sohana Akbar
Sohana Akbar

Posted on

How to Cache API Responses at the CDN Level Without Breaking Auth

Let's be honest: caching API responses at the CDN level can feel like walking a tightrope. On one side, you have the promise of blazing-fast responses and massive cost savings. On the other, you have the terrifying prospect of serving User A's private data to User B.

It doesn't have to be that way. With the right strategy, you can have your cake and eat it too—caching aggressively while keeping authentication rock-solid.

The Core Problem
The fundamental tension here is straightforward: CDNs are shared caches. When a response for an authenticated user gets stored in a shared cache, another user requesting the same URL might receive that private response. This is one of the most frequent and dangerous mistakes in API caching.

If the content is personalized, don't let it be shared!

But "don't cache authenticated content" is a lazy cop-out. Many API responses are partially cacheable—public data that happens to be requested by authenticated users. The real challenge is splitting the difference.

Strategy 1: The Header-First Approach
Your first line of defense is getting HTTP headers right. This is non-negotiable.

For Sensitive, User-Specific Data
If you're serving truly private data (user profiles, account settings, financial info), use Cache-Control: no-store. This tells every cache along the chain—browser proxies, corporate proxies, and CDNs—to absolutely never store any part of the request or response.

http
Cache-Control: no-store
For User-Specific But Safe-to-Cache-in-Browser Data
Some data is user-specific but safe for the user's own browser to cache. Think of a user's dashboard or preferences. Use Cache-Control: private to allow browser caching while explicitly forbidding CDNs and other shared caches from storing it.

http
Cache-Control: private, max-age=3600
The Vary Header: Your Secret Weapon
This is where it gets interesting. The Vary header tells the CDN that the response for a given URL depends on specific request headers.

http
Vary: Cookie, Accept-Language
When you include Vary: Cookie, the CDN caches separate versions of the response for different cookie values. This means authenticated users get their own cached copies, and public users get theirs—no mixing!

Crucial warning: The Financial Times team validates JWTs at the edge, then parses authentication data from the token. They split this data into separate headers—user ID for totally personalized content, user role for cacheable-by-role content. This granularity is key: if the backend only needs a user's role or group membership to generate a response, the response can be cached and reused for multiple users with that role.

Strategy 2: Authentication at the Edge
This is where things get really powerful. Instead of sending every request to your origin for authentication, perform auth checks at the CDN edge.

How It Works
The Financial Times puts authentication logic directly on their CDN using Fastly's VCL. When a request comes in with an authentication cookie, the CDN decodes and verifies the JWT signature without contacting the origin server.

vcl
// Simplified example of JWT verification at the edge
if (req.http.Cookie:NikkeiAuth) {
// Decode and verify the JWT signature
// Extract user ID, rank, and other claims
// Set trusted headers for the origin
set req.http.Nikkei-UserID = "...";
set req.http.Nikkei-Rank = "...";
}
By doing authentication at the edge, you:

Protect your origin from auth-related traffic spikes

Reduce latency by eliminating an extra round trip before responding

Enable granular caching based on auth data

But the signature verification step is critical. Without it, people could forge credentials. Use HMAC-SHA256 or your CDN's crypto functions to validate tokens before acting on them.

Strategy 3: Selective Caching by Authentication Status
A pragmatic approach: different cache rules for authenticated vs. unauthenticated requests.

The Nhost team implemented middleware that sets CDN-Cache-Control headers based on whether a request carries authentication headers:

go
if hasAuthHeaders {
// Authenticated: revalidate every time
value = "must-revalidate, no-cache"
} else {
// Unauthenticated: cache aggressively
value = "max-age=86400, public"
}
This pattern is simple and effective. Public requests get cached for a day. Authenticated requests bypass the cache or revalidate on every request. No mixing, no leaks.

Strategy 4: Cache Key Normalization
Default cache keys typically include URL + query string. But you need to be intentional about what matters.

Include What Matters
Query parameters that alter the response (e.g., ?locale=en)

Relevant headers (accept, content-type)

Auth-related headers (but carefully—you don't want the token itself in the cache key)

Exclude What Doesn't
Tracking parameters (utm_*, ref, random session IDs)

Headers that don't affect the response

Everything that creates unnecessary cache fragmentation

Canonicalization
Use edge logic to normalize requests. Strip irrelevant headers, sort query parameters, and map multiple request variations to a canonical cache key.

Strategy 5: Surrogate Keys for Invalidation
Sometimes you need to invalidate cached responses across many users. This is where surrogate keys shine.

Your origin includes a Surrogate-Key header on responses:

http
Surrogate-Key: product-123 category-electronics region-us
The CDN tags the cached response with these keys. When content changes, you call the CDN purge API with the key to invalidate all responses tagged with it—for all users, across all variations.

Putting It All Together: A Practical Strategy
Here's a battle-tested approach:

Step 1: Profile Your Endpoints

Which endpoints return public data? Cache aggressively.

Which return user-specific data? Cache with Vary: Cookie.

Which return truly sensitive data? Use no-store.

Step 2: Normalize at the Edge

Remove tracking params

Canonicalize query ordering

Strip irrelevant headers

Step 3: Set Intelligent Cache Rules

Public GET requests: Cache-Control: public, max-age=3600

Authenticated GETs: Cache-Control: private, max-age=300 + Vary: Cookie

Sensitive data: Cache-Control: no-store

Step 4: Implement Purge Strategies

Use surrogate keys for related content

Version your API (/v1/, /v2/) to avoid needing instant purges for major changes

Step 5: Monitor Cache Performance

Track cache hit/miss ratios per endpoint

Monitor origin request rates

Set up alerts for cache behavior anomalies

The Bottom Line
Caching authenticated API responses at the CDN level isn't just possible—it's a proven performance strategy. The Financial Times, Nhost, and countless others do it in production.

The secret sauce is understanding that "authenticated" doesn't mean "uncacheable." It means "cache with care." Use the right headers, validate at the edge when you can, and always, always be explicit about what varies the response.

Your users will thank you for the speed. Your infrastructure team will thank you for the reduced origin load. And your security team will thank you for not leaking private data.

What's your approach to API caching? Drop your strategies in the comments—I'd love to hear what's working for you!

Top comments (0)