All articles
DevOps & CloudJuly 27, 2026 6 min read

CloudFront to Vercel: The Cache Header Mismatch That Cost Us a Weekend

We fronted a Vercel app with CloudFront to satisfy a compliance requirement. Two weeks later, stale checkouts and missing Set-Cookie headers taught us how differently these two CDNs think about caching.

A compliance clause forced us to route a client's production traffic through AWS CloudFront before it hit their Vercel deployment. On paper: trivial. In practice: two weeks of intermittent stale checkouts, vanished session cookies, and a Slack thread that got long enough to deserve its own retrospective.

If you're about to put CloudFront (or any origin CDN) in front of Vercel, read this first. The two systems have different opinions about what a cache header means, and nobody warns you until users are calling support.

Why we did it in the first place

The client's parent org had a security policy that required all inbound traffic to pass through an AWS WAF ruleset owned by their central platform team. That WAF was attached to a CloudFront distribution. Vercel has its own WAF now, but "use ours" wasn't a negotiation we were going to win.

So the topology became:

User -> Route53 -> CloudFront (WAF) -> Vercel Edge -> Serverless Functions

Two CDNs stacked. Two cache layers. Two sets of header semantics. What could go wrong.

The initial setup that looked fine

We pointed CloudFront's origin at the Vercel deployment's *.vercel.app hostname, set the host header override, forwarded the Authorization and Cookie headers, and used the AWS-managed CachingOptimized policy for static assets and CachingDisabled for everything else. Smoke tests passed. Lighthouse looked good. We shipped it on a Thursday.

The first symptom: stale product pages

By Saturday, support had three tickets about product pages showing sold-out items as available. The Vercel deployment was returning fresh data — we confirmed by hitting the *.vercel.app origin directly. CloudFront was serving a cached copy that was about six hours old.

The culprit was subtle. Our Next.js app was setting:

Cache-Control: public, s-maxage=60, stale-while-revalidate=300

Vercel's edge respects s-maxage and stale-while-revalidate beautifully. CloudFront respects s-maxage. CloudFront does not honor stale-while-revalidate as a revalidation directive the way Vercel does — it treats the response as cacheable for the s-maxage window, then falls back to its own min/max TTL configuration for anything after that.

The AWS-managed CachingOptimized policy has a default TTL of 24 hours and a max TTL of one year. When our s-maxage=60 expired, CloudFront wasn't revalidating on every request the way we assumed. It was applying its own TTL logic on top.

The fix, and why it's annoying

We wrote a custom CloudFront cache policy with:

  • Min TTL: 0
  • Default TTL: 0
  • Max TTL: 31536000
  • Origin cache headers: honored

That pushes all TTL decisions to the origin. Combined with an explicit Cache-Control on every route in the Next.js app, behavior became predictable. But it means you can no longer rely on the managed policies — every team touching the distribution has to know that the origin is authoritative.

The second symptom: disappearing sessions

About a week in, users started getting logged out mid-checkout. Not all users. Not consistently. The kind of bug that makes you question your career.

CloudFront was stripping Set-Cookie headers on cached responses. This is documented, but it's the kind of documentation you only read after the incident. If a response is cacheable and you haven't explicitly included Set-Cookie in the response headers policy, CloudFront drops it — because caching a response with a user-specific cookie would be a catastrophic cross-user leak.

The problem was that our auth callback route was returning Cache-Control: no-store, which we assumed made it uncacheable and therefore safe. CloudFront's behavior here is defensive: even for uncacheable responses, Set-Cookie isn't forwarded unless the response headers policy allows it, in some configurations. We were tripping over an interaction between the origin request policy and the response headers policy that we hadn't fully modeled.

What actually worked

We split the distribution into two behaviors:

# Terraform sketch — not the full config
ordered_cache_behavior {
  path_pattern             = "/api/auth/*"
  cache_policy_id          = aws_cloudfront_cache_policy.no_cache.id
  origin_request_policy_id = aws_cloudfront_origin_request_policy.all_viewer.id
  response_headers_policy_id = aws_cloudfront_response_headers_policy.pass_cookies.id
  viewer_protocol_policy   = "redirect-to-https"
  allowed_methods          = ["GET", "HEAD", "OPTIONS", "PUT", "POST", "PATCH", "DELETE"]
}

default_cache_behavior {
  cache_policy_id          = aws_cloudfront_cache_policy.origin_authoritative.id
  origin_request_policy_id = aws_cloudfront_origin_request_policy.minimal.id
  viewer_protocol_policy   = "redirect-to-https"
}

The all_viewer origin request policy forwards everything to Vercel. The response headers policy explicitly allows Set-Cookie through. For all other traffic, we forward the minimum — no cookies, no auth headers — so those responses stay genuinely cacheable and shared across users.

That separation is the mental model we should have started with: any behavior that touches user identity is a completely different beast from a behavior that serves shared content. Vercel handles this distinction implicitly through its routing and cache tags. CloudFront makes you spell it out.

The third symptom: Vercel's analytics went dark

Once CloudFront was in the path, Vercel's Web Analytics stopped attributing traffic correctly. The requests hitting Vercel's edge all came from a small pool of CloudFront IPs, so geographic and device data collapsed.

The fix was forwarding CloudFront-Viewer-Country, CloudFront-Viewer-City, and the real User-Agent as custom headers, then having a small middleware layer on Vercel read those instead of the request's apparent origin. Not hard, just another thing nobody documents in the "put a CDN in front of your app" tutorial because that tutorial doesn't exist.

The bill nobody warned us about

CloudFront to Vercel is egress from AWS to a third party. Every request pays AWS egress rates on the response body, then pays again on Vercel's bandwidth meter. For a static-heavy site this can double your CDN costs compared to running on Vercel alone.

In our case, roughly 60% of the traffic was static assets we could have served straight from CloudFront by uploading builds to S3, bypassing Vercel entirely for those paths. We didn't do that — the client wanted a single deploy pipeline — but it's the tradeoff to think about before you sign the architecture off.

What we'd tell the next team

A few rules we now enforce on any project stacking CloudFront in front of Vercel (or in front of any modern platform CDN, honestly):

  1. Origin is authoritative for TTL. Custom cache policy, min/default TTL of zero, max TTL high. Never use CachingOptimized for dynamic origins.
  2. Split behaviors by identity boundary. Auth routes and any route that sets cookies get their own behavior with no-cache policy and an explicit response headers policy that allows Set-Cookie.
  3. Forward viewer metadata explicitly. If you care about analytics or geo-routing, plumb the CloudFront-Viewer-* headers through to your app.
  4. Model the cost twice. AWS egress plus Vercel bandwidth is not the same math as Vercel alone. Get a real traffic sample from the first week and reforecast.
  5. Test with real cookies, not curl. Half our bugs would have surfaced in a Playwright suite that logged in, waited five minutes, and tried to check out. Curl-based smoke tests missed all of them.

Where we'd start

If you're staring down this exact requirement, start with a spike: put CloudFront in front of a staging Vercel deployment for a week with realistic synthetic traffic, and instrument both layers. Use the Vercel logs to see what's reaching origin, and CloudFront's real-time logs to see what's being served from cache. The gap between those two numbers is where your bugs live.

And if the compliance requirement is negotiable — check whether Vercel's own WAF and BYOC options (their enterprise networking docs cover this) satisfy the actual control the security team cares about. Removing a CDN layer is almost always cheaper than debugging one.

#Vercel#AWS#CloudFront#CDN#Caching#DevOps

Want a team like ours?

72Technologies builds production software for the kind of teams who actually read this blog.

Start a project