Edge Computing with AWS: CloudFront and Lambda@Edge
We use CloudFront + Lambda@Edge for specific patterns. The wins, the production gotchas, and where we hit Lambda@Edge's limits.
Key takeaways
- We use CloudFront + Lambda@Edge for specific patterns.
- The wins, the production gotchas, and where we hit Lambda@Edge's limits.
On this page
Edge Computing with CloudFront and Lambda@Edge
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.
What edge actually means here#
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).
Patterns where edge wins#
Five patterns we use in production:
1. URL rewriting and redirects at the edge#
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.
2. A/B testing assignment#
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.
3. Authentication gate#
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.
4. Header rewriting#
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.
5. Geographic routing#
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.
What's limited at the edge#
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.
What we tried and abandoned#
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.
Operational realities#
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.
CloudFront Functions vs Lambda@Edge#
When to use each:
CloudFront Functions:
- Trivial logic (header manipulation, URL rewrites, A/B bucket assignment without state)
- Low latency requirements (sub-millisecond)
- High volume (cheap per invocation)
Lambda@Edge:
- Real logic (DB calls, JWT validation, complex business rules)
- Need full Node.js or Python runtime
- Need npm packages
We use CloudFront Functions for ~80% of our edge logic; Lambda@Edge for the harder cases.
The cost reality#
For us, at our traffic volume:
- CloudFront Functions: ~$15/month total across all functions
- Lambda@Edge: ~$200/month total
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).
What I'd tell a team starting#
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.
Stay Updated
Get the latest tutorials, guides, and insights on AI, DevOps, Cloud, and Infrastructure delivered directly to your inbox.
Cloud-Native Databases: Choosing the Right Database for Your Workload
Postgres, DynamoDB, Redis, Elasticsearch, Snowflake. We use all five for different workloads. The decision criteria, not the marketing comparison.
Systemd and Modern Linux Service Management
Run services reliably with systemd: units, dependencies, and resource limits.
More from Cloud
Explore more articles in this category
Best Serverless Databases in 2026 (Compared)
A practitioner comparison of the leading serverless databases by use case, cold-start behavior, branching, pricing model, and lock-in.
Cloudflare D1: The Edge SQLite Database Guide (2026)
A practitioner's look at Cloudflare D1, the serverless SQLite database built for Workers, covering setup, read replication, limits, and fit.
Neon vs PlanetScale: Serverless SQL Compared (2026)
A practitioner comparison of Neon's serverless Postgres against PlanetScale's Vitess-backed MySQL to help you pick the right database.
You might have missed
Evergreen posts worth revisiting.