Deploying a Single Page Application (SPA) to AWS CloudFront often leads to a frustrating "version mismatch" problem. You push new code to S3, but users still see the old version because the CDN edge locations are serving cached files. Worse, if a user's browser loads an old index.html that points to a deleted JavaScript bundle, the entire application crashes. Achieving zero-downtime deployment requires a precise orchestration between your CI/CD pipeline, file naming conventions, and CloudFront invalidation logic.
This guide provides a production-ready blueprint for ensuring your users always receive the latest version of your React, Vue, or Angular application without performance degradation or "White Screen of Death" errors. You will learn how to combine immutable file hashing with targeted invalidations to optimize both cost and user experience.
TL;DR — Use content hashing for all static assets (JS, CSS, images) and set their Cache-Control to 1 year. Never invalidate these. Only invalidate the index.html file during deployment to point users to the new hashed assets immediately.
The Core Concept: Immutable Assets vs. Mutable Entry Points
index.html is the physical menu card (the entry point), while the hashed files like main.a1b2c3.js are the specific ingredients in the kitchen. If you change the ingredients but keep the same menu card, the waiter will try to serve items that no longer exist. To fix this, you don't throw away the ingredients; you simply swap the menu card for a new one that lists the updated ingredients.
In a modern SPA architecture, files fall into two categories. The first category is Static Assets. These include your compiled JavaScript bundles, CSS files, and images. Modern build tools like Vite or Webpack automatically append a unique hash to these filenames based on their content (e.g., app.782hsd.js). Because the filename changes whenever the code changes, these files are "immutable." You can cache them at the edge for a year because a file with that exact name will never change its content.
The second category is the Entry Point, which is almost always your index.html. This file is "mutable" because its name stays the same, but its content changes to point to the new hashed asset names. Zero-downtime invalidation focuses entirely on this file. By telling CloudFront to clear the cache for only index.html, you force the browser to fetch the new "map" of your application, which then pulls in the new immutable assets from the CDN or S3 bucket.
When to Use This Strategy
You should adopt this strategy if you are running a production-grade web application where user session continuity is critical. If your deployment process involves deleting the entire contents of an S3 bucket before uploading new files, you are likely causing downtime. Users who have the old index.html cached in their browser will click a link, the SPA will try to lazy-load a JavaScript chunk that you just deleted, and the application will fail silently or crash.
This approach is also essential for high-traffic sites where CloudFront invalidation costs are a concern. AWS allows 1,000 free invalidation paths per month. If you use a wildcard invalidation (/*) for every commit, you will quickly exceed this limit and incur charges. By targeting only the root file, you remain within the free tier while ensuring that every user gets the latest code updates within seconds of a deployment finishing.
How to Implement Zero-Downtime Invalidation
Step 1: Configure Your Build Tool for Hashing
Ensure your build configuration generates unique hashes for all assets. In Vite, this is the default behavior. For Webpack, ensure your output.filename uses [contenthash]. This ensures that if App.tsx changes, the resulting .js file gets a brand new name.
// vite.config.js example
export default {
build: {
rollupOptions: {
output: {
entryFileNames: `assets/[name].[hash].js`,
chunkFileNames: `assets/[name].[hash].js`,
assetFileNames: `assets/[name].[hash].[ext]`
}
}
}
}
Step 2: Atomic S3 Upload Strategy
When deploying, never use the --delete flag in the aws s3 sync command as your first step. Instead, upload the new files alongside the old ones. This ensures that users currently on the site can still load the old chunks they need until they refresh the page.
# 1. Upload new assets (don't delete old ones yet)
aws s3 sync ./dist s3://my-bucket-name --acl public-read
# 2. Set long-term cache for hashed assets
aws s3 cp s3://my-bucket-name/assets s3://my-bucket-name/assets \
--recursive --metadata-directive REPLACE \
--cache-control "max-age=31536000, immutable"
Step 3: Trigger Targeted CloudFront Invalidation
Once the files are safely in S3, trigger an invalidation for /index.html. In modern AWS CLI versions, this happens almost instantly across the global network. Using the --paths flag with specific files is much faster than a full directory scan.
# 3. Invalidate only the entry point
aws cloudfront create-invalidation \
--distribution-id YOUR_DIST_ID \
--paths "/index.html"
Common Pitfalls and Error Fixes
/* as an invalidation path. While this seems safe, it clears the cache for every single image and script in your app. This forces CloudFront to pull 100% of your site from S3 (the origin) for the next few minutes, leading to a massive latency spike and potentially higher S3 egress costs.
A frequent issue is the "Access Denied" error during invalidation. This typically occurs because the IAM user or role running the CI/CD pipeline lacks the cloudfront:CreateInvalidation permission. Ensure your GitHub Actions or GitLab Runner role has a policy that specifically targets your Distribution ARN. Avoid giving AdministratorAccess; use the principle of least privilege.
Another pitfall is Metadata Overwriting. If you upload your files and then realize the Cache-Control headers are wrong, simply re-running the sync command might not update the metadata in S3. You must use the cp command with --metadata-directive REPLACE as shown in Step 2 to force S3 to update the headers without re-uploading the entire file content.
Optimization Tips for Cost and Performance
To maximize performance, utilize CloudFront Cache Policies instead of legacy headers. Specifically, the "Managed-CachingOptimized" policy is designed to handle compressed files (Brotli/Gzip) and query strings correctly. When you use this policy, CloudFront automatically handles the negotiation with the browser to serve the smallest possible version of your hashed assets.
For cost management, remember that AWS provides 1,000 free paths per month. If you have a staging, UAT, and production environment all on the same AWS account, those invalidations add up. I recommend implementing a "deployment gate" that only triggers invalidation if the index.html hash has actually changed. You can calculate the MD5 hash of your local index.html and compare it against the S3 object metadata before calling the CloudFront API.
In my experience building React apps for enterprise clients, the most effective way to verify a successful zero-downtime deployment is to check the X-Cache header in the browser's Network tab. After invalidation, the first request to index.html should return Miss from cloudfront, and subsequent requests should return Hit from cloudfront with the updated content.
Frequently Asked Questions
Q. How long does a CloudFront invalidation take?
A. Historically, invalidations took 10-15 minutes. However, with modern AWS infrastructure, a single-file invalidation for index.html typically completes in 10 to 60 seconds. You can monitor the status via the AWS Console or CLI.
Q. Should I invalidate my CSS and JS files too?
A. No. If you use content hashing (e.g., styles.a9f2.css), the filename is unique to that version. Since the old file still exists in S3, invalidating it is unnecessary and wastes your free tier quota.
Q. Is there a way to avoid invalidation costs entirely?
A. Yes, by setting the Cache-Control of index.html to no-cache, no-store, must-revalidate. This forces the browser and CloudFront to check the origin every time. While this saves invalidation effort, it increases latency for the initial page load.
📌 Key Takeaways
- Always use content hashing for JS, CSS, and images.
- Upload new files to S3 without deleting old versions immediately to support active sessions.
- Invalidate only the entry point (
index.html) to trigger an app update. - Set
Cache-Control: immutablefor hashed assets to maximize CDN performance. - Use specific paths in your
aws cloudfront create-invalidationcommand to stay within the free tier.
By following this strategy, you ensure that your SPA deployments are invisible to the user, providing a smooth transition between application versions while maintaining the high performance of the AWS global edge network. For more information, refer to the official AWS CloudFront Invalidation documentation.
Post a Comment