In AWS architectures, there is a silent trade-off that often goes unnoticed—until your AWS bill spikes or your latency graphs degrade:
Personalization vs. Cache Efficiency
The common approach to A/B testing—executing logic in a viewer-request hook and varying the cache key using cookies—seems convenient, but it is an architectural trap.
By introducing variants into the cache key, you:
- Fragment your global edge cache
- Destroy your Cache Hit Ratio (CHR)
- Increase S3 origin load and egress costs
- Reintroduce cold-start latency for users
There is a better way.
🧠 The Solution: The Shadow Origin Pattern
The Shadow Origin Pattern is a technique where the public request URI remains unchanged, while the origin fetch path is internally rewritten after a cache miss. Instead of exposing variants to the CDN cache key, we move the decision behind the cache layer.
1. The Architecture: Performance-First Routing
The goal is simple: Do not fragment the cache key at the edge.
How it works
-
Public Request (Cache Key):
/shop -
Internal Origin Fetch (after cache miss):
/variants/B/shop
This is achieved using a Lambda@Edge origin-request hook, which runs only after CloudFront determines the object is not in cache.
Why this is powerful
- The cache key remains clean and stable.
- Variants are resolved internally.
- Each variant is cached efficiently once fetched.
- No unnecessary duplication of edge cache entries.
2. The Implementation: The “Shadow” Hook
This Lambda@Edge function acts as a precise traffic controller.
/**
* Lambda@Edge: Origin Request Trigger
* Goal: Internal URI mutation for high-CHR A/B Testing
*/
'use strict';
// Remove this line in AWS production
exports.hookType = 'origin-request';
exports.handler = async (event) => {
const request = event.Records[0].cf.request;
const { headers } = request;
// 1. Determine User Segment
let variant = 'control';
if (headers.cookie) {
for (let i = 0; i < headers.cookie.length; i++) {
if (headers.cookie[i].value.includes('X-Variant=B')) {
variant = 'B';
break;
}
}
}
// 2. Internal Path Mutation
if (variant === 'B') {
request.uri = `/variants/B${request.uri}`;
}
// 3. S3 OAC Normalization
if (request.uri.endsWith('/')) {
request.uri += 'index.html';
}
return request;
};
💡 Save this file as `ab-testing.js`
⚠️ Critical AWS Configuration (Do Not Skip)
This function runs at the origin-request stage. Unlike viewer-request, cookies are not automatically available. You must configure CloudFront to forward them:
-
Cache Policy: Cookies -> Include (or whitelist
X-Variant). -
Origin Request Policy: Cookies -> Include (or whitelist
X-Variant).
If missed, your logic silently defaults to variant = 'control', and your A/B test will appear broken.
3. The Fidelity Gap: Why Testing is Critical
The biggest barrier to adopting this pattern is what we call the Fidelity Gap. Everything happens inside AWS infrastructure; your browser "lies" to you because it only sees the public URI.
This leads to the classic (and painful) workflow: Deploy → Wait 20 minutes → Discover a 403 → Repeat.
Closing the Gap with Local Simulation
To eliminate this blind spot, we can simulate the environment locally using CloudFrontize.
Step 1: Mock the S3 Origin
Recreate your production structure in a local /public folder:
/public
├── index.html
└── variants/
└── B/
└── index.html
Step 2: Run the CloudFrontize simulator
Emulate Lambda@Edge behavior locally with the --debug flag to see the "Invisible" rewrite:
cloudfrontize ./public --edge ./ab-testing.js --debug
Step 3: Verify the Invisible Rewrite
- Open
http://localhost:3000/. - Set the variant cookie in the browser console:
document.cookie = "X-Variant=B";
location.reload();
What you should see in the terminal:
[origin-request] Original URI: /
[Debug] Rewriting URI: / -> /variants/B/index.html
Your browser still shows /, but the content has changed. The Shadow Origin Pattern is working.
4. Professional Gotchas (Hard Lessons)
-
No Environment Variables: Lambda@Edge does not support
process.env. Your function must be self-contained. - The 40KB Limit: Large cookies can break your function unexpectedly.
-
Cache Invalidation: Invalidating
/shopdoes not invalidate/variants/B/shop. You must invalidate both. - Regex Latency: On high-traffic sites, complex regex adds latency. Prefer simple string operations.
🧾 Summary
High-performance edge architectures require discipline: keep the cache key clean and move logic behind the cache layer. By applying the Shadow Origin Pattern and validating it locally with CloudFrontize, you eliminate the deployment "black box" and gain full control over your edge behavior.
Next Step: Are you ready to bridge the fidelity gap? Check out the CloudFrontize GitHub Repository for more edge patterns.
Top comments (0)