DEV Community

Cover image for CloudFront A/B Testing Without Cache Fragmentation: The Shadow Origin Pattern
felipecarrillo100
felipecarrillo100

Posted on • Originally published at github.com

CloudFront A/B Testing Without Cache Fragmentation: The Shadow Origin Pattern

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;
};

Enter fullscreen mode Exit fullscreen mode

💡 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:

  1. Cache Policy: Cookies -> Include (or whitelist X-Variant).
  2. 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

Enter fullscreen mode Exit fullscreen mode

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 

Enter fullscreen mode Exit fullscreen mode

Step 3: Verify the Invisible Rewrite

  1. Open http://localhost:3000/.
  2. Set the variant cookie in the browser console:
document.cookie = "X-Variant=B";
location.reload();

Enter fullscreen mode Exit fullscreen mode

What you should see in the terminal:

[origin-request] Original URI: /
[Debug] Rewriting URI: / -> /variants/B/index.html

Enter fullscreen mode Exit fullscreen mode

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 /shop does 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)