I needed one S3 bucket to serve assets to multiple domains. The request worked for one origin and then failed for another because the cached response carried the first site’s CORS header. The missing piece was Vary: Origin.
Say you have assets in S3 that example.com and another.com both use. S3 returns Access-Control-Allow-Origin: example.com to the first request. A CDN caches that response. When another.com requests the same asset, it gets the cached response with the wrong origin, and the browser blocks it. Vary: Origin tells caches: “This response changes based on the Origin header. Don’t reuse it for different origins.”
The configuration
S3 generates Vary: Origin automatically when you configure multiple CORS rules:
<?xml version="1.0" encoding="UTF-8"?>
<CORSConfiguration xmlns="http://s3.amazonaws.com/doc/2006-03-01/">
<CORSRule>
<AllowedOrigin>http*</AllowedOrigin>
<AllowedMethod>GET</AllowedMethod>
<AllowedHeader>*</AllowedHeader>
</CORSRule>
<CORSRule>
<AllowedOrigin>*</AllowedOrigin>
<AllowedMethod>GET</AllowedMethod>
<AllowedMethod>HEAD</AllowedMethod>
</CORSRule>
</CORSConfiguration>
In the S3 console, go to the bucket’s Permissions -> CORS configuration and use this XML. The first rule matches http:// and https:// origins and returns the specific origin in Access-Control-Allow-Origin. The second handles simple GET and HEAD requests with * as a fallback. Together, the rules make S3 vary the response by origin.
Testing it
Send a preflight-style request with both headers:
curl -sI \
-H "Origin: https://kw.sg" \
-H "Access-Control-Request-Method: GET" \
https://s3.amazonaws.com/animate-vpaid-bridge/sample-1.xml
Response:
HTTP/1.1 200 OK
Access-Control-Allow-Origin: https://kw.sg
Vary: Origin, Access-Control-Request-Headers, Access-Control-Request-Method
Notice Access-Control-Allow-Origin: https://kw.sg (the specific origin) and Vary: Origin. Now send a simple request with just the Origin header:
curl -sI \
-H "Origin: https://kw.sg" \
https://s3.amazonaws.com/animate-vpaid-bridge/sample-1.xml
Response:
HTTP/1.1 200 OK
Access-Control-Allow-Origin: *
Vary: Origin, Access-Control-Request-Headers, Access-Control-Request-Method
Now it returns Access-Control-Allow-Origin: * (wildcard), but Vary: Origin is still present. Caches will still differentiate responses by origin.
Field tip S3 only emits
Vary: Originwhen your CORS configuration has multiple rules. A singleAllowedOrigin: *rule won’t produce it, breaking CDN caching for multi-origin setups.
The separate http* and * rules are what triggered this behavior in my setup.
You need this when multiple sites load assets from the same S3 bucket, you’re using a CDN in front of S3, or you support credentials in cross-origin requests. If your assets are public and only used by one site, a simpler AllowedOrigin: * config works fine.
For a deeper dive into how CORS works and how to configure it in Apache and nginx, see my post on understanding CORS basics.
In 2025, I moved away from S3 to Cloudflare Workers for static hosting, which handles CORS configuration more simply and deploys to the edge globally.
One quick signal
Did this earn your time?
Thanks. That gives me something concrete to check.



