CloudFront Now Supports Tag-Based Cache Invalidation, So I Tried It Out
This page has been translated by machine translation. View original
On April 29, 2026, Amazon CloudFront added support for Invalidation by cache tags.
With conventional CloudFront Invalidation, you could only specify URL paths individually, or use a wildcard (/*) to clear the cache over a broad range. With this update, you can attach tags to objects via origin response headers and then perform bulk Invalidation by specifying those tags.
I built a test environment with an S3 origin and CloudFormation, and checked the actual behavior.
How cache tag Invalidation works
Based on the official documentation, let's go over how it works.
Get started in 3 steps
- Configure
CacheTagConfigon the distribution: Specify the name of the HTTP header from which tags are read - The origin attaches tags to the response headers: Multiple tags can be specified, separated by commas
- Invalidate with
#tag-name: Specify it with the#prefix, like--paths "#version:v1"
With an S3 origin, you can use it just by setting tags in the object's metadata. S3 automatically returns it as an x-amz-meta-<key-name> header.
S3オブジェクトのメタデータ:
cache-tag: "page:products, version:v1, category:electronics"
↓ S3が自動変換
レスポンスヘッダー:
x-amz-meta-cache-tag: page:products, version:v1, category:electronics
↓ CloudFrontが読み取り
キャッシュタグとして保存: [page:products] [version:v1] [category:electronics]
Tag specifications
| Item | Limit |
|---|---|
| Maximum number of tags per object | 50 (excess is ignored) |
| Maximum length of one tag | 256 characters |
| Allowed characters | ASCII 33–126 (spaces, commas, and control characters are not allowed) |
| Case sensitivity | Case-insensitive |
※ A tag value itself cannot contain commas or spaces. Commas are used as delimiters between tags, and spaces before and after the delimiters are trimmed during parsing. In other words, tag1,tag2 and tag1, tag2 are treated as equivalent.
Pricing
One tag = one path for billing purposes. The pricing structure is the same as conventional path Invalidation (the first 1,000 paths per month are free, then $0.005 per path).
The key point is that whether 100 or 10,000 objects are associated with a tag, one tag specification = one path charge.
Quotas
| Item | Limit |
|---|---|
| Rate of path or tag submissions | 150 per second (paths and tags combined) |
| Wildcard Invalidation | 1 per second |
| Number of files processed per tag | Unlimited |
Tags share the same quota as paths, and one tag counts as one path. For details, see the official quota page.
Building the test environment (CloudFormation)
We'll build the test environment with CloudFormation. CacheTagConfig is supported in CloudFormation.
CloudFormation template
AWSTemplateFormatVersion: '2010-09-09'
Description: CloudFront Cache Tag Invalidation Test Stack
Resources:
S3Bucket:
Type: AWS::S3::Bucket
Properties:
BucketName: !Sub 'cf-cache-tag-test-${AWS::AccountId}'
BucketPolicy:
Type: AWS::S3::BucketPolicy
Properties:
Bucket: !Ref S3Bucket
PolicyDocument:
Version: '2012-10-17'
Statement:
- Sid: AllowCloudFrontServicePrincipal
Effect: Allow
Principal:
Service: cloudfront.amazonaws.com
Action: s3:GetObject
Resource: !Sub '${S3Bucket.Arn}/*'
Condition:
StringEquals:
AWS:SourceArn: !Sub 'arn:aws:cloudfront::${AWS::AccountId}:distribution/${Distribution}'
OAC:
Type: AWS::CloudFront::OriginAccessControl
Properties:
OriginAccessControlConfig:
Name: !Sub 'cache-tag-test-oac-${AWS::AccountId}'
OriginAccessControlOriginType: s3
SigningBehavior: always
SigningProtocol: sigv4
Distribution:
Type: AWS::CloudFront::Distribution
Properties:
DistributionConfig:
Comment: Cache Tag Invalidation Test
Enabled: true
DefaultRootObject: index.html
CacheTagConfig:
HeaderName: x-amz-meta-cache-tag
Origins:
- Id: S3Origin
DomainName: !GetAtt S3Bucket.RegionalDomainName
OriginAccessControlId: !Ref OAC
S3OriginConfig:
OriginAccessIdentity: ''
DefaultCacheBehavior:
TargetOriginId: S3Origin
ViewerProtocolPolicy: redirect-to-https
Compress: true
ForwardedValues:
QueryString: false
DefaultTTL: 3600
MinTTL: 180
MaxTTL: 3600
Outputs:
BucketName:
Value: !Ref S3Bucket
DistributionId:
Value: !Ref Distribution
DomainName:
Value: !GetAtt Distribution.DomainName
The core setting of this update is CacheTagConfig.
Type: AWS::CloudFront::Distribution
Properties:
DistributionConfig:
CacheTagConfig:
HeaderName: x-amz-meta-cache-tag
The S3 metadata key cache-tag is returned as the x-amz-meta-cache-tag header, so that is what I specified for HeaderName.
For the cache settings, I used the legacy configuration (ForwardedValues) rather than the managed cache policy (CachingOptimized). During testing, with CachingOptimized, when the S3 origin did not return a Cache-Control header, CloudFront would query the origin with conditional requests, and caching behaved unstably. By setting MinTTL to 180 seconds with the legacy configuration and also attaching Cache-Control: max-age=300 on the S3 side, I was able to confirm stable caching behavior while respecting the origin's caching directives.
Deployment
aws cloudformation deploy \
--stack-name cf-cache-tag-test \
--template-file template.yaml \
--region us-east-1
Uploading test content
Upload S3 objects with metadata (cache tags) and a Cache-Control header attached.
aws s3api put-object --bucket "$BUCKET" --key products.html \
--body products.html --content-type text/html \
--cache-control "max-age=300" \
--metadata '{"cache-tag":"page:products, version:v1, category:electronics"}'
Tags are specified, separated by commas, in the cache-tag key of --metadata. The other files are uploaded in the same way.
Verification
We'll check the behavior with two test cases.
First, get the distribution ID from the CloudFormation Outputs.
DIST_ID=$(aws cloudformation describe-stacks \
--stack-name cf-cache-tag-test \
--query "Stacks[0].Outputs[?OutputKey=='DistributionId'].OutputValue" \
--output text)
Test setup
| File | Tags |
|---|---|
| index.html | page:home, version:v1 |
| products.html | page:products, version:v1, category:electronics |
| about.html | page:about, version:v1 |
All three files share the version:v1 tag, and the tags starting with page: differ for each file.
TEST A: Bulk Invalidation with a common tag (#version:v1)
All three files have the version:v1 tag. After warming the cache by accessing each file several times, we run Invalidation specifying this tag.
aws cloudfront create-invalidation \
--distribution-id "$DIST_ID" \
--paths "#version:v1"
You simply specify a tag name with the # prefix instead of a path. Because # is interpreted as a comment by the shell, it must be enclosed in double quotes.
Results
| File | Before Invalidation | After Invalidation |
|---|---|---|
| index.html | Hit | Miss ✅ |
| products.html | Hit | Miss ✅ |
| about.html | Hit | Miss ✅ |
※ X-Cache: Miss from cloudfront indicates that the cache was invalidated and the object was fetched again from the origin.
The cache of all files was invalidated. It took about 18 seconds from issuing the Invalidation to completion (the official documentation says under 25 seconds at P95).
TEST B: Selective Invalidation (#page:products)
Next, after warming the cache again, we run Invalidation specifying the page:products tag. Only products.html has this tag.
aws cloudfront create-invalidation \
--distribution-id "$DIST_ID" \
--paths "#page:products"
Results
| File | Before Invalidation | After Invalidation |
|---|---|---|
| index.html | Hit | Hit ✅ |
| products.html | Hit | Miss ✅ |
| about.html | Hit | Hit ✅ |
Only products.html's cache was invalidated, while index.html and about.html remained cache hits. It took about 22 seconds from issuing to completion.
This is the decisive difference from conventional wildcards (/products/*). By grouping semantically with tags, you avoid clearing the cache of unrelated content.
Comparison with the conventional method
| Conventional (path specification) | Cache tags | |
|---|---|---|
| How to specify | /products/123.html or /products/* |
#brand:acme |
| Precision | Depends on path structure. Easily catches unrelated content | Invalidates only what's needed through semantic grouping |
| Operational burden | Need to track and enumerate target URLs | Once the app attaches tags, one request completes it |
| Cache hit ratio | Tends to drop with wildcards | Stays high because unrelated cache remains |
| Origin load | Re-fetches occur for everything over-cleared | Only minimal re-fetching is needed |
| Cost | Enumerating individual URLs is billed per path. A wildcard is one path, but origin transfer costs increase | One tag = one path charge. Flat regardless of the number of associated objects. Origin transfer costs are also minimal |
The essential change brought by cache tags is that the decision of "what to clear" shifts from URL structure to application semantics (brand, version, user, etc.).
Tag design tips
The official documentation introduces the following example use cases.
| Use case | Example tags | Example Invalidation |
|---|---|---|
| E-commerce product catalog | category:electronics, brand:acme, product:12345 |
Invalidate all ACME products at once with #brand:acme |
| User-generated content | user:12345, content-type:image |
Delete all content of a departed user at once with #user:12345 |
| Content versioning | version:v2, template:homepage |
Invalidate all v2 content at once on deployment with #version:v2 |
All of them are unified in the key:value format. Following this, if you establish rules for prefixes in your own project, you won't be confused during operation. Since multiple tags can be attached to one object, you can perform Invalidation across multiple axes.
Notes
- Opt-in required: Tags are ignored on distributions where
CacheTagConfigis not set - Be careful when changing the header name: If you change the header name in
CacheTagConfig, tag Invalidation will no longer work on objects cached with the old header. A path Invalidation (/*) is needed before the change - Tag changes are not reflected in existing cache: Even if you change the tags in S3 metadata, the tags of objects already cached in CloudFront are not updated. To invalidate with a new tag, you first need to delete the existing cache with a path Invalidation and have responses with the new tags cached again
- When CacheTagConfig is removed: Even after removal, existing cache continues to be served as usual until the TTL expires
- Backward compatibility: Conventional path and wildcard Invalidation remain available
- All variants are affected: When you invalidate by tag, all cache variants by cookie, header, and query string are also invalidated. You cannot leave only specific variants
Summary
CloudFront now supports Invalidation by cache tags, enabling flexible cache management that doesn't depend on URL structure.
In this verification, we set tags in S3 object metadata and confirmed that #version:v1 invalidated three files at once, and #page:products selectively invalidated just one file. You can avoid over-clearing the cache with wildcards, keep the cache hit ratio high, and precisely invalidate only the necessary content.
It also supports CloudFormation and can be used together with conventional path Invalidation, so it can be adopted gradually.
This time I tested with an S3 origin, but it is even more effective with a CMS origin that serves dynamic content.
For example, suppose you configure the CMS to return tags such as article:12345, category:tech, author:suzuki in the response headers when an article is updated. This lets you reflect bulk updates of articles in a specific category, or the replacement of profile information following a change in an author's affiliation, immediately with a single request such as #category:tech or #author:suzuki.
Because you can minimize the lag in content updates while keeping unrelated cache intact, it is expected to be useful in situations where you were struggling with cache strategy for a CMS and CloudFront combination.
Invalidation by author tags and category tags
I uploaded four articles to S3 and confirmed selective Invalidation by author tags and category tags.
Additional verification (TEST C / TEST D)
Upload four article pages with author and category tags attached.
| File | Tags |
|---|---|
| articles/001.html | article:001, category:compute, author:suzuki |
| articles/002.html | article:002, category:networking, author:suzuki |
| articles/003.html | article:003, category:database, author:tanaka |
| articles/004.html | article:004, category:storage, author:tanaka |
TEST C: Bulk Invalidation by author tag (#author:suzuki)
aws cloudfront create-invalidation --distribution-id "$DIST_ID" --paths "#author:suzuki"
| Article | Author | After Invalidation |
|---|---|---|
| 001 (Intro to Lambda) | suzuki | Miss ✅ |
| 002 (CloudFront Design) | suzuki | Miss ✅ |
| 003 (DynamoDB in Practice) | tanaka | Hit ✅ |
| 004 (S3 Security) | tanaka | Hit ✅ |
Only suzuki's two articles were invalidated, while tanaka's articles remained cache hits.
TEST D: Selective Invalidation by category tag (#category:database)
aws cloudfront create-invalidation --distribution-id "$DIST_ID" --paths "#category:database"
| Article | Category | After Invalidation |
|---|---|---|
| 001 | compute | Hit ✅ |
| 002 | networking | Hit ✅ |
| 003 | database | Miss ✅ |
| 004 | storage | Hit ✅ |
For the same group of articles, we confirmed that we can invalidate only the targeted articles along either the author axis or the category axis.
