CloudFront Invalidation Cost Math, Wildcard Limits, and Why Fingerprinting Makes Most of It Irrelevant
You’ve just run your hundredth deploy of the month and received a bill line from AWS you weren’t expecting. Or you submitted an invalidation request and got a TooManyInvalidationsInProgress error with no immediate obvious cause. Or you’re evaluating whether to keep using /* as your standard invalidation path or switch to more surgical targeting. All three situations come back to the same underlying question: what does CloudFront invalidation actually cost, at what scale does it become a problem, and how do fingerprinted asset URLs change the calculus entirely?
This page covers the exact cost model, the 15-concurrent-invalidation limit and how to work around it, wildcard path counting semantics, and the arithmetic that shows why teams shipping fingerprinted assets rarely need to think about invalidation costs at all — except for the small set of HTML entry points that cannot be fingerprinted.
The Cost Model: What CloudFront Actually Charges
AWS charges for CloudFront invalidations based on the number of paths submitted, not the number of objects actually removed from edge caches. The pricing has two tiers:
- First 1,000 invalidation paths per month: free. This allowance applies across all CloudFront distributions in the account, not per distribution.
- Beyond 1,000 paths: $0.005 per path. That is half a cent per additional path submitted after the first thousand.
The monthly counter resets on the first day of each calendar month, UTC.
What Counts as One Path
This is where teams frequently make incorrect assumptions. A single submitted path — regardless of whether it is a literal file path or a wildcard — counts as exactly one path for billing purposes.
/assets/app.js— 1 path/assets/*— 1 path/*— 1 path/images/hero.png— 1 path
Submitting /* causes CloudFront to remove every cached object from all edge locations for that distribution. You might be invalidating 50,000 distinct files. AWS charges for one path.
This means the common fear of “wildcards will cost me more” is unfounded when you are already within the free tier. If you are submitting /* once per deploy and doing fewer than 1,000 deploys per month, the total invalidation cost is zero. The wildcard is not a billing multiplier — it is an object-selection mechanism, and billing only counts the path string you submitted.
The Arithmetic at Common Deploy Frequencies
| Deploys per month | Strategy | Paths used | Cost (USD) |
|---|---|---|---|
| 50 | /* per deploy |
50 | $0.00 |
| 200 | /* per deploy |
200 | $0.00 |
| 800 | /* per deploy |
800 | $0.00 |
| 1,000 | /* per deploy |
1,000 | $0.00 |
| 1,200 | /* per deploy |
1,200 | $1.00 |
| 5,000 | /* per deploy |
5,000 | $20.00 |
| 200 | /index.html only |
200 | $0.00 |
| 5,000 | /index.html only |
5,000 | $20.00 |
The last two rows illustrate something important: if you are deploying 5,000 times per month, even a single-path invalidation per deploy hits $20. That is still not catastrophic, but it is non-zero. High-frequency continuous-deployment pipelines — feature flag toggles, preview environments, hotfix chains — can breach the free tier more easily than teams expect.
The 15-Concurrent-Invalidation Limit
Cost is only part of the picture. The harder operational constraint is the 15-concurrent-invalidation limit per distribution.
CloudFront restricts each distribution to at most 15 invalidation requests simultaneously in InProgress or Completed (but recently submitted) states. Submitting a 16th before enough earlier ones finish returns:
TooManyInvalidationsInProgress: Your request contains too many invalidation paths.
(The error name is slightly misleading — the issue is the count of in-flight requests, not paths per request.)
Checking Current In-Progress Invalidations
Before submitting a new invalidation in a script or CI pipeline, verify the current queue depth. The following command lists all invalidations for a distribution, filtered to those still in progress:
DIST_ID="E1EXAMPLE123456"
aws cloudfront list-invalidations \
--distribution-id "$DIST_ID" \
--query 'InvalidationList.Items[?Status==`InProgress`].[Id,Status,CreateTime]' \
--output table
If the count returned is 15, your next submission will fail. You have two options: wait for at least one to complete, or consolidate multiple paths into a single invalidation request (which counts as one request regardless of how many path strings it contains — up to 3,000 paths per single request).
Polling in a CI pipeline looks like this:
DIST_ID="E1EXAMPLE123456"
wait_for_invalidation_slot() {
local max_attempts=30
local attempt=0
while [ $attempt -lt $max_attempts ]; do
local in_progress
in_progress=$(aws cloudfront list-invalidations \
--distribution-id "$DIST_ID" \
--query 'length(InvalidationList.Items[?Status==`InProgress`])' \
--output text)
if [ "$in_progress" -lt 15 ]; then
echo "Slot available ($in_progress in progress). Proceeding."
return 0
fi
echo "15 invalidations in progress. Waiting 10s (attempt $((attempt+1))/$max_attempts)..."
sleep 10
attempt=$((attempt+1))
done
echo "Timed out waiting for invalidation slot." >&2
return 1
}
wait_for_invalidation_slot && aws cloudfront create-invalidation \
--distribution-id "$DIST_ID" \
--paths "/index.html" "/404.html"
This pattern is especially useful in parallel deployment pipelines where multiple environments share a single CloudFront distribution and may submit invalidations concurrently.
Invalidate-All vs. Versioned Paths: Side-by-Side Comparison
The choice between invalidating everything after each deploy versus fingerprinting assets and invalidating only HTML entry points changes every dimension of the problem.
| Dimension | Invalidate /* on every deploy |
Fingerprinted assets + invalidate HTML only |
|---|---|---|
| Paths per deploy | 1 (wildcard) | 1–5 (HTML entry points) |
| Cost at 100 deploys/month | $0.00 (well within free tier) | $0.00 |
| Cost at 1,500 deploys/month | $2.50 | $2.50 |
| Risk of serving stale JS/CSS | High during invalidation propagation (30–60 seconds) | Near zero — stale JS/CSS URL simply never requested |
| Risk of HTML/JS version mismatch | Present — edge may serve new HTML with old cached JS | Eliminated — new HTML points to new hash URLs |
| Rollback complexity | Must re-invalidate after reverting deploy | Redeploy old build; old hash URLs re-emerge automatically |
| CDN cache efficiency | All assets re-cached after every deploy | Unchanged assets remain cached indefinitely |
max-age you can safely use on JS/CSS |
Short (minutes to hours) to limit staleness window | max-age=31536000, immutable |
| Concurrent invalidation slot pressure | 1 slot consumed per deploy | 1 slot consumed per deploy |
Cost parity is visible at most deploy frequencies because wildcard counting means both strategies consume approximately the same number of paths. The structural advantage of fingerprinting is not cost savings on invalidation — it is cache longevity and correctness. When a JavaScript file’s content has not changed between deploys, its hash has not changed, its URL has not changed, and it remains warm in every edge cache worldwide. No invalidation propagation delay, no brief window where users receive new HTML referencing a JS file that is still cached under the old response.
Strategy Decision Diagram
Why Fingerprinting Makes Most Invalidation Unnecessary
The core insight behind content hashing is that when the content of a file changes, its URL changes. A JavaScript bundle that was previously served at /assets/app.a3f8c291.js becomes /assets/app.d71b04e9.js after a code change. The old URL still works — it still points to a valid, unchanged object in S3 — and the new URL is simply new. No cached object becomes stale. No edge location holds a copy of a file that needs replacing.
This means for any asset served under a content-hash URL, you never need to submit an invalidation request. The asset either exists in the cache (and is correct, because the hash guarantees it) or it does not (and CloudFront fetches it from the origin). Both outcomes are correct.
What does need invalidation is the HTML file itself — /index.html, /404.html, and any other non-fingerprinted entry point. HTML documents reference specific hash-versioned asset URLs, and the HTML document’s content changes on every deploy. Since HTML files are rarely fingerprinted themselves (their URL must remain predictable for browsers, search engines, and link sharing), they are served without immutable cache headers and must be invalidated after each deploy.
A typical production HTML invalidation after a deploy with fingerprinted assets:
DIST_ID="E1EXAMPLE123456"
INVALIDATION_ID=$(aws cloudfront create-invalidation \
--distribution-id "$DIST_ID" \
--paths "/index.html" "/404.html" "/sitemap.xml" \
--query 'Invalidation.Id' \
--output text)
echo "Invalidation submitted: $INVALIDATION_ID"
# Optional: wait for completion before marking deploy done
aws cloudfront wait invalidation-completed \
--distribution-id "$DIST_ID" \
--id "$INVALIDATION_ID"
echo "Invalidation complete. HTML documents propagated to all edge locations."
This submits two or three paths, costs nothing at any realistic deploy frequency, and leaves every JS, CSS, image, and font file untouched in edge caches globally.
The hash length matters here too. Default build tool configurations often emit 8 hex characters (e.g., app.a3f8c29.js). For monorepos where many packages build simultaneously and collisions across a large artifact set are theoretically more likely, increasing to 12 or 16 characters provides additional safety margin — though even 8 hex characters gives 4 billion unique values and collision probability is negligible in practice. The cache key architecture page covers the tradeoffs of hash length in detail.
One Verification Command
After submitting an invalidation, confirm it is progressing and check how many slots remain available:
DIST_ID="E1EXAMPLE123456"
aws cloudfront list-invalidations \
--distribution-id "$DIST_ID" \
--query 'InvalidationList.Items[].[Id,Status,CreateTime]' \
--output table
The Status field transitions from InProgress to Completed. Once Completed, the slot is freed and the 15-in-progress limit no longer counts that request. On average, a CloudFront invalidation completes across all edge locations within 30–60 seconds, though AWS does not guarantee a specific time.
To narrow the output to only in-progress requests and count them:
aws cloudfront list-invalidations \
--distribution-id "$DIST_ID" \
--query 'length(InvalidationList.Items[?Status==`InProgress`])' \
--output text
A return value of 0 means you have the full 15 slots available.
When Invalidation IS the Right Choice
Fingerprinting eliminates the need for invalidation on most assets, but several scenarios make invalidation the correct tool regardless of whether assets are fingerprinted.
Security incident response. If a compromised JavaScript file reached production and needs to be removed from edge caches immediately — regardless of its URL — /* invalidation is the fastest mechanism. Even if the file is fingerprinted and the compromised version has been removed from origin, cached copies at edge locations will serve the bad file until their TTL expires or you invalidate. In a security incident, submit /* and do not wait for TTLs.
Serving non-fingerprinted files that changed in place. Some files cannot be fingerprinted: robots.txt, manifest.json, favicon.ico, and any URL referenced in advertising or analytics systems that you cannot change. These files change in place and require targeted invalidation after each update.
Correcting origin configuration errors. If you served incorrect Cache-Control headers for a period and want to reset edge behavior, an invalidation forces a fresh fetch that picks up the correct headers going forward.
Emergency hotfixes on short-TTL HTML. If your HTML files are served with a short TTL (60–300 seconds) rather than a zero TTL, there is a window between deploy and TTL expiry where old HTML is served. An invalidation closes that window immediately. With fingerprinted assets, this only creates a brief mismatch between HTML and assets — the old HTML still references valid (old) hash URLs — but for time-critical fixes, forced invalidation is appropriate.
Testing cache behavior. During initial setup and CDN purge strategy configuration, you may want to reset edge state repeatedly to verify that origin changes propagate correctly. Using invalidation as a development tool is fine as long as you are not near the 1,000-path monthly limit.
The distinction is: invalidation is a corrective mechanism for exceptional situations. Fingerprinting is a structural solution that eliminates the need for correction in the first place on assets whose content is the source of truth for their identity.
Related
- CloudFront Invalidation Guide — parent page covering the full CloudFront invalidation workflow, API reference, and CI integration
- CDN Purge Strategies — overview of purge approaches across CloudFront, Cloudflare, Fastly, and Nginx
- Cloudflare Cache Rules and Purge — how Cloudflare’s cache rule system compares to CloudFront’s invalidation model
- Content Hashing vs. Semantic Versioning — why content-addressed URLs are structurally superior to version-suffixed ones