We use CloudFront + Lambda@Edge for specific patterns. The wins, the production gotchas, and where we hit Lambda@Edge's limits.
Edge computing is the marketing umbrella over a useful but constrained set of capabilities. We've used CloudFront with Lambda@Edge for several years for specific patterns: A/B testing at the edge, header rewrites, auth gates, and personalization. This is what those patterns look like in production, the limits we've hit, and where we'd reach for something else.
The "edge" is the CloudFront point-of-presence (POP) — there are 600+ globally. Code that runs at the edge runs there, close to the user, before the request even reaches your origin (your actual servers).
Two main flavors:
Lambda@Edge: regular Lambda functions deployed to CloudFront. Runs at one of four CloudFront events (viewer-request, origin-request, origin-response, viewer-response). Limited runtime constraints; up to 30 seconds; 10MB package size.
CloudFront Functions: a more constrained, lower-latency alternative. JavaScript only, ~1ms execution, very limited capabilities. Cheaper.
Use CloudFront Functions for trivial header manipulation. Use Lambda@Edge when you need real logic (DB calls, fetch other services).
Five patterns we use in production:
User requests /de/about → rewrite to /about?lang=de and forward to origin. Or, redirect old URLs to new ones without round-tripping to origin.
Implementation:
function handler(event) {
const request = event.request;
if (request.uri.startsWith("/de/")) {
request.uri = request.uri.substring(3);
request.querystring.lang = { value: "de" };
}
return request;
}
(CloudFront Functions syntax — runs at viewer-request.)
This saves a round-trip to origin for what would otherwise be a 301. At our traffic, that's millions of round-trips saved per day.
User has no cookie → edge function assigns them to a bucket (50/50 split), sets a cookie, lets request continue.
async function handler(event) {
const request = event.request;
const cookie = request.headers.cookie;
if (!cookie?.value?.includes("ab_bucket=")) {
const bucket = Math.random() < 0.5 ? "A" : "B";
request.headers["x-ab-bucket"] = { value: bucket };
// origin will set the cookie in response
} else {
// pull bucket from cookie
}
return request;
}
Why at the edge: the assignment happens before any cache lookup, so different buckets can get different cached responses. Without edge logic, the user's bucket would be assigned at origin, which means CloudFront would cache one response and serve the same bucket to everyone.
Protected paths require a JWT. The edge function validates the JWT before forwarding to origin:
async function handler(event) {
const request = event.request;
if (request.uri.startsWith("/admin/")) {
const authHeader = request.headers.authorization?.value;
if (!authHeader || !verifyJWT(authHeader)) {
return { statusCode: 401, statusDescription: "Unauthorized" };
}
}
return request;
}
Why at the edge: rejected requests never hit origin. For DDoS-prone endpoints, this isolates origin from invalid traffic.
Set security headers (CSP, HSTS, X-Frame-Options, etc.) on every response. Easier to enforce universally at the edge than to trust every backend service.
function handler(event) {
const response = event.response;
response.headers["strict-transport-security"] = { value: "max-age=63072000; includeSubDomains" };
response.headers["x-content-type-options"] = { value: "nosniff" };
response.headers["x-frame-options"] = { value: "DENY" };
return response;
}
(Runs at viewer-response.)
We've audited and removed the equivalent header-setting code from various backend services. One place to manage headers is much cleaner.
Different regions → different origins. We route US traffic to our us-east-1 origin and EU traffic to our eu-west-1 origin based on the CloudFront-Viewer-Country header.
function handler(event) {
const request = event.request;
const country = request.headers["cloudfront-viewer-country"]?.value;
if (["DE", "FR", "ES", "IT", "GB"].includes(country)) {
request.origin = { /* ... eu origin config */ };
}
return request;
}
This is a different pattern than DNS-level geographic routing — happens after the request reaches CloudFront, so works with edge cache and other edge logic.
The constraints that hurt:
No outbound HTTPS to arbitrary destinations from CloudFront Functions. Lambda@Edge can do it but at higher latency and cost than at-origin.
No persistent connections. Each invocation gets a new "environment." Database connections can't be pooled across invocations the way they can in regular Lambda.
Strict size limits. CloudFront Functions: 10KB function size. Lambda@Edge: 10MB. Heavyweight dependencies (full SDKs, etc.) don't fit.
Limited regions. Lambda@Edge runs only in a subset of regions; some debugging requires logging to a CloudWatch in those regions.
Cold starts hurt. Edge cold starts are real. For Lambda@Edge, ~100ms cold start per POP per function. CloudFront caches functions across invocations, but the first request to a POP hits cold.
Pricing complexity. Lambda@Edge is billed per request × duration × memory. CloudFront Functions are cheaper but more limited. The math matters at scale.
Patterns we tried at the edge that didn't pan out:
Heavy personalization at the edge. Tried doing personalization (recommend products based on user history) at the edge. The DB call to fetch user data added latency that defeated the purpose. Moved the personalization to origin.
Image manipulation at the edge. Resizing/transforming images at the edge sounded good. Lambda@Edge has package-size limits that made this impractical for most image libraries. We use a separate origin (an image-resizing service) instead.
Edge-side rate limiting. Tried rate limiting at the edge (count requests per IP, reject above threshold). The "count" needs persistence; without a shared store, each POP counts independently. AWS WAF rate limiting is a better fit for this.
Complex routing logic. A multi-step routing decision tree at the edge became hard to maintain. Moved to a simpler edge function that forwards to a routing service at origin.
The pattern: edge is great for fast, stateless, deterministic logic. As soon as you need state, external services, or complex code, the edge constraints push back.
Things that have surprised us:
Deploys take time. Deploying a Lambda@Edge function requires CloudFront to propagate it to all POPs. ~15 minutes. Slower than regular Lambda's ~30 seconds.
Versioning is unusual. You can't update a Lambda@Edge function in place; you create a new version and update the CloudFront association. Rollback means re-associating the previous version.
Monitoring is decentralized. Logs from Lambda@Edge go to CloudWatch in the region closest to the POP. To see all logs, you query multiple regions. We use Athena over the consolidated logs for cross-region queries.
Cold starts compound. A function that's only invoked once per POP per hour has ~100% cold-start rate. The "edge cache it" doesn't apply to functions like it does to content.
Debugging is harder. Local testing with the actual request lifecycle is awkward. We have a test harness that mocks the CloudFront events; it's good but not perfect.
When to use each:
CloudFront Functions:
Lambda@Edge:
We use CloudFront Functions for ~80% of our edge logic; Lambda@Edge for the harder cases.
For us, at our traffic volume:
The Lambda@Edge cost is dominated by a couple of high-traffic functions that do JWT validation. If we needed to optimize, we'd port them to CloudFront Functions (which doesn't support JWT signing crypto natively, but we could verify JWTs that don't need signature verification at the edge).
CloudFront Functions for the simple stuff, Lambda@Edge for the rest. Don't reach for Lambda@Edge when CloudFront Functions suffice.
Edge logic should be stateless. State means external calls; external calls mean latency; latency defeats the edge purpose.
Headers, redirects, A/B assignment are slam-dunk wins. These are what edge is for.
Don't try to do origin work at the edge. Personalization, complex routing, image transformation — these usually belong at origin.
Plan for slower deploys. ~15 minute propagation is the trade for global reach.
Centralize header management at the edge. One place to maintain security headers across all responses is much cleaner than scattering them across backend services.
Edge computing is a useful set of tools when used for the right patterns. The marketing pitch (run anything at the edge!) is overstated. The practical reality (run trivial-but-globally-useful logic close to users) is genuinely useful and saves real round-trips. Use it for that; don't try to make it more.
Get the latest tutorials, guides, and insights on AI, DevOps, Cloud, and Infrastructure delivered directly to your inbox.
A field report from rolling out retrieval-augmented generation in production, including cache bugs, bad embeddings, and how we fixed them.
Practical game day scenarios for CI/CD: broken rollbacks, permission issues, and slow feedback loops—and how we fixed them.
Explore more articles in this category
There are two hard problems in computer science." We've worked on the cache-invalidation one for a while. The patterns that hold up at scale and the ones that look clean and aren't.
We use Step Functions for batch processing, document ingestion, and a few agentic workflows. The patterns that work, the limits we hit, and where we'd reach for something else.
After two years of running Karpenter on production EKS clusters, the NodePool patterns that survived, the ones we replaced, and the tuning that matters.