Skip to content
bluegreen

Blue-Green Deployments for a Static Site: What CloudFront Actually Guarantees

·TechAWSCloudFrontDeployment

Almost everything written about blue-green deployment assumes you have a fleet of servers behind a load balancer. You stand up the new version, health-check it, flip the target group, done. CodeDeploy has a whole feature for this. ALB will happily shift traffic for you in stages. It’s a solved problem, and the solution is well documented because AWS’s own tooling was built around exactly that shape of infrastructure.

None of that applies when your “application” is a folder of HTML, JS, and CSS sitting in an S3 bucket with CloudFront in front of it. There’s no compute to drain, no health check endpoint, no target group. I built a version of this at Quince for a Gatsby storefront, and the first thing that becomes obvious is that “blue-green for a static site” isn’t a smaller version of the server problem. It’s a different problem, and AWS’s docs don’t really treat it as its own thing — you assemble it from the origin failover page and the invalidation page, both written for different use cases.

What actually needs to change during a “swap”

A static deploy has two moving parts: the objects sitting in S3, and whatever CloudFront has cached at its edge locations. Blue-green for this setup means: build goes to a fresh location (a second bucket, or a new prefix in the same bucket), you verify it however you verify it, and then you need viewers to start getting the new build instead of the old one — ideally without a window where some fraction of requests get half-old, half-new assets.

There are really only two ways to make that cutover happen:

  1. Change which origin (or origin path) the distribution points at. You update the distribution config — via the API, CLI, or IaC — to point the relevant cache behavior at the new bucket or the new prefix, and CloudFront starts pulling fresh content on the next cache miss.
  2. Use a CloudFront origin group and lean on its failover behavior to move traffic from the “blue” origin to the “green” one.

Most people reach for option 2 first because “origin group” sounds exactly like what you want. It isn’t, and the docs are pretty clear about why once you read them closely.

Origin groups are a failover feature, not a cutover feature

CloudFront’s origin failover documentation describes an origin group as a primary and a secondary origin, where CloudFront sends every request to the primary first and only falls back to the secondary when the primary returns a status code you’ve configured as a failure trigger (you pick from 400, 403, 404, 416, 429, 500, 502, 503, and 504) or the connection to it times out. By default CloudFront tries the primary for up to 30 seconds (three attempts, 10 seconds each) before giving up — tunable down to a few seconds, but never instant. And failover only happens for GET, HEAD, and OPTIONS requests; once one request fails over, that doesn’t stick, every new request tries the primary again first.

Read that with a deploy cutover in mind. To use an origin group for blue-green, you’d need “blue” to start returning one of those failure codes on demand, on every request, so CloudFront treats it as permanently down. That’s not a config flip, that’s making S3 lie about being broken. It’s workable in a pinch (a bucket policy returning 403, say) but it’s a hack bolted onto a feature built for actual origin outages, not planned cutovers. The semantics — a handful of status codes, up to 30 seconds by default, only certain HTTP methods — make total sense for “my origin died” and don’t map onto “move traffic to version 2 right now.”

The path that’s actually used in practice, and the one I’d point you to, is repointing the origin or origin path on the distribution’s cache behavior directly. Two S3 buckets (or one bucket, two prefixes like /releases/blue/ and /releases/green/), and the “swap” is a distribution config update: origin path changes from /blue to /green, deployed through CloudFormation/CDK/Terraform like any other infra change. No status codes to fake, no failover logic fighting you.

The part that actually bites you: cache, not compute

Here’s the diagram-in-words version of the setup: viewers hit a CloudFront distribution with one cache behavior, that behavior points at an origin path, the origin path currently resolves to bucket-prefix “blue.” You finish building “green,” update the origin path, and now new cache misses at any edge location pull from green. Simple enough — except CloudFront has hundreds of edge locations around the world, each with its own cache, and none of them know your origin path changed until either the cached object expires (Cache-Control/TTL) or you invalidate it.

This is where the CloudFront docs are more useful, and more sobering, than the failover page. The invalidation documentation is upfront that AWS doesn’t consider invalidation the primary tool here: “if you want to update your files frequently, we recommend that you primarily use file versioning” instead, because a viewer (or a corporate proxy) that already has the old file cached keeps serving it until it expires locally, invalidation or not. On propagation, the docs say a submitted request is “forwarded to all edge locations within a few seconds” and each edge “starts processing the invalidation immediately” — also why you can’t cancel one once it’s running. Notice what that does and doesn’t promise: it commits to the request reaching every edge fast, and says nothing about every edge finishing the purge at the same moment. Close enough to instant that nobody notices for one HTML file. Less so when you’re invalidating a lot of paths, or a wildcard, right when traffic is highest — which, for a storefront, is exactly when you’re tempted to ship a fix.

There are hard limits here too: 150 invalidation paths per second, and only one wildcard invalidation per second, per the quotas page. Fire off /* on a big site during a release window and you can genuinely queue yourself behind your own invalidations.

What this means for the actual cutover: don’t invalidate the world. Fingerprint your JS/CSS bundle filenames (Gatsby, Webpack, and friends do this out of the box) and cache those basically forever — a new build produces new filenames, which is precisely the “versioned file names” approach the docs prefer, and none of it ever needs invalidating. Set HTML entry points to a short Cache-Control max-age or no-cache with revalidation, so they roll over fast on their own. The only thing you invalidate on a real cutover is a handful of entry-point paths, not the whole tree, and you accept that for a stretch of seconds to maybe a minute, some edges are still serving whichever HTML was cached, pointing at whichever bundle was live at the time. Immutable asset names are what keep that overlap harmless — a stale HTML page still resolves to JS that actually exists.

Worth knowing about but not a fit here: CloudFront has a continuous deployment feature with staging distributions that shifts a percentage of live traffic to a second distribution. It’s built for testing distribution configuration changes — headers, behaviors, policies — against real traffic before promoting them, not for versioned content cutovers. Different tool, adjacent problem, easy to conflate with what you actually need.

AWS’s general blue/green whitepaper is honest that it’s written around traffic-shifting mechanisms like Route 53 weighted routing and ALB, and it’s explicitly flagged as historical at this point. None of it mentions CDN edge caches, because for a server fleet, cache isn’t the constraint. For a static site, it’s the entire game. The swap mechanism — origin path, origin group, whatever — is the easy five minutes. Getting your cache headers and invalidation scope right so the swap is actually clean at the edge is the part that takes a few release cycles to get right, and it’s the part nobody’s landing page tells you about.

Sources