Cloudflare Workers vs AWS Lambda@Edge for Request Routing
Both Cloudflare Workers and AWS Lambda@Edge let you run code at the network edge to inspect, rewrite, and route requests before they touch your origin. They are not interchangeable: they differ in where code runs, how it starts, what it can do per request, and how you ship it. After reading this guide you will be able to choose the right platform for a specific routing workload, write the equivalent handler on either side, and verify the routing decision in production logs.
Key objectives:
- Understand the execution models (V8 isolates vs full Node runtime, cold starts) and where each platform runs your code.
- Map the event hooks each platform exposes so you attach routing logic at the correct phase.
- Compare hard limits (CPU time, memory, package size) and pricing against your traffic shape.
- Apply a decision framework and verify a routing decision with curl and provider logs.
Execution model: V8 isolates vs full runtime
The single largest architectural difference is how each platform isolates and starts your code. Cloudflare Workers run inside V8 isolates, the same sandboxing primitive a browser uses to keep tabs apart. Thousands of isolates share one process, so spinning up your Worker for a new request is a matter of microseconds and there is effectively no cold start. The trade-off is that you write to a web-standard runtime (fetch, Request, Response, Web Crypto, URL), not full Node.js. Most npm packages that touch the filesystem or raw TCP will not run unmodified.
AWS Lambda@Edge runs your function inside a conventional Lambda micro-VM (Firecracker) with a real Node.js runtime. You get the Node standard library, a writable /tmp, and most npm packages. The cost is cold starts: when a region has no warm container, AWS provisions one, and you pay a startup penalty that ranges from tens of milliseconds to over a second for heavy bundles. For latency-sensitive routing that penalty lands directly on the first user in each region.
Where the code physically runs also differs. A Cloudflare Worker executes in the same POP that terminated the TLS connection, so it runs in all of Cloudflare’s locations. Lambda@Edge does not run in every CloudFront point of presence; AWS replicates your function to a smaller set of regional edge caches, and viewer-facing requests are served from POPs that route back to those regions. The practical effect is that Workers tend to put compute closer to the user, while Lambda@Edge concentrates it in fewer locations. If your routing logic depends on tight tail latency, this matters as much as raw runtime speed. For a deeper look at deploying routing logic on the Cloudflare side, see deploying Cloudflare Workers for dynamic request routing.
Event hooks: where you attach routing logic
A Worker has one entry point for HTTP: the fetch handler. You receive a Request, do whatever you want, and return a Response — either synthesized at the edge or proxied from origin. Routing, rewriting, header mutation, and caching all happen in that single function, which keeps the mental model simple.
Lambda@Edge splits the request lifecycle into four hooks tied to the CloudFront cache:
| Hook | Fires | Sees cache result | Typical routing use |
|---|---|---|---|
| viewer-request | Before cache lookup, every request | No | A/B routing, auth redirects, normalize path |
| origin-request | On cache miss, before origin | After miss | Pick origin, rewrite path to backend |
| origin-response | After origin replies | n/a | Mutate origin headers before caching |
| viewer-response | Before reply to client | n/a | Add security headers on every response |
The split is powerful: putting heavy logic on origin-request means it only runs on cache misses, which can dramatically cut invocations on a cache-friendly site. The cost is that you must reason about which hook owns a decision, and a misplaced handler can run far more often than you expected. Workers collapse all of this into one function, so you control “run on miss only” yourself by checking the cache inside fetch.
Limits that shape what you can build
The constraints below decide whether a workload is even feasible before pricing enters the picture. Treat them as the first filter.
| Constraint | Cloudflare Workers | AWS Lambda@Edge (viewer) | AWS Lambda@Edge (origin) |
|---|---|---|---|
| Runtime | V8 isolate, web standards | Node.js / Python micro-VM | Node.js / Python micro-VM |
| CPU time (paid) | up to ~30s configurable | 5 s | 30 s |
| Memory | 128 MB | 128 MB | up to 10 GB |
| Deploy/bundle size | ~10 MB (gzipped) per Worker | 1 MB | 50 MB |
| Cold start | effectively none | tens of ms to ~1 s | tens of ms to ~1 s |
| Request/response body edit | full streaming | limited body size | larger body allowed |
| Env vars / secrets | bindings, KV, secrets | no env vars at edge | no env vars at edge |
A subtle Lambda@Edge gotcha: functions deployed to the edge cannot use environment variables, so configuration has to be baked into the bundle or fetched at runtime. Workers handle this cleanly with bindings and secrets, which is one reason dynamic-config routing is easier there. The tiny 1 MB viewer-request package limit also rules out pulling in large SDKs at the hook that fires on every request.
A routing snippet on each platform
The two handlers below do the same job: route requests under /beta to a canary backend and everything else to the stable origin, while stamping a header so you can confirm the decision downstream. First, the Cloudflare Worker.
// worker.js — runs in a V8 isolate at every POP
export default {
async fetch(request) {
const url = new URL(request.url);
const isBeta = url.pathname.startsWith("/beta");
const origin = isBeta
? "https://canary.origin.example.com"
: "https://stable.origin.example.com";
// Rewrite the hostname, keep path + query intact
const target = new URL(url.pathname + url.search, origin);
const proxied = new Request(target, request);
proxied.headers.set("x-route-target", isBeta ? "canary" : "stable");
const resp = await fetch(proxied);
const out = new Response(resp.body, resp);
out.headers.set("x-routed-by", "cf-worker");
return out;
},
};
Deploy it with wrangler deploy and bind it to a route. Now the equivalent Lambda@Edge viewer-request handler in Node.
// index.js — Lambda@Edge viewer-request, Node 20 runtime
export const handler = async (event) => {
const request = event.Records[0].cf.request;
const isBeta = request.uri.startsWith("/beta");
// Repoint the origin for this request
request.origin = {
custom: {
domainName: isBeta
? "canary.origin.example.com"
: "stable.origin.example.com",
port: 443,
protocol: "https",
sslProtocols: ["TLSv1.2"],
readTimeout: 30,
keepaliveTimeout: 5,
customHeaders: {},
},
};
// Host header must follow the new origin
request.headers["host"] = [{ key: "Host", value: request.origin.custom.domainName }];
request.headers["x-route-target"] = [
{ key: "X-Route-Target", value: isBeta ? "canary" : "stable" },
];
return request;
};
The shapes diverge in instructive ways. The Worker owns the full request/response cycle and returns a Response; the Lambda handler mutates an event object and hands it back to CloudFront, which performs the actual fetch. Setting a custom origin on viewer-request requires CloudFront to allow origin override, and the Host header must be rewritten or the origin will reject the request. If you are weighing this against a third option, the Vercel Edge vs Cloudflare Workers performance comparison covers a closely related isolate-based runtime.
Pricing model
Cloudflare Workers bill on requests plus CPU time (not wall-clock), with a flat-rate paid plan that bundles a generous request allowance and then a low per-million overage. Because isolates have no cold start and you only pay for CPU actually consumed, idle waits on fetch to origin do not accrue compute charges.
Lambda@Edge bills on request count plus GB-seconds of duration, and the per-request and per-duration rates carry an edge premium over standard Lambda. Crucially, duration is wall-clock: time spent awaiting the origin on origin-request is billed. The mitigation is the cache — logic placed on origin-request only fires on misses, so a high cache-hit ratio can make Lambda@Edge cheaper than its raw rates suggest. Model your real hit ratio before comparing; for a cache-heavy static site the invocation count can be a fraction of total traffic.
Developer workflow
Cloudflare’s loop is a single CLI. wrangler dev runs the Worker locally against the real runtime, wrangler deploy ships it globally in seconds, and wrangler tail streams live logs. There is no version-association dance — a deploy is atomically global.
Lambda@Edge is multi-step. You publish a numbered Lambda version (not $LATEST), associate that version with a CloudFront distribution behavior and the chosen event type, then wait for the distribution to deploy across the network, which can take several minutes. Rollback means re-associating an earlier version and waiting again. Logs land in CloudWatch in the AWS region nearest the POP that served the request, so debugging a single user can mean hunting across multiple regional log groups.
| Workflow step | Cloudflare Workers | AWS Lambda@Edge |
|---|---|---|
| Local dev | wrangler dev |
SAM / manual event JSON |
| Deploy | wrangler deploy (seconds, global) |
publish version + associate + propagate (minutes) |
| Logs | wrangler tail (real-time) |
CloudWatch, per-region log groups |
| Rollback | redeploy prior bundle | re-associate prior version |
Decision framework: when to pick each
Pick Cloudflare Workers when you want the lowest tail latency at every POP, no cold starts, fast iteration, dynamic configuration via bindings or KV, and a web-standard runtime is acceptable. It is the better default for auth checks, A/B routing, header rewriting, and any logic that must run on every request quickly.
Pick AWS Lambda@Edge when you are already invested in CloudFront and the AWS ecosystem, need a full Node or Python runtime with larger memory or npm packages, want logic that only fires on cache misses via origin-request, or must keep traffic and IAM inside AWS for compliance. The four-hook model is also a genuine advantage when different concerns naturally belong at different lifecycle phases.
Need full Node runtime / big npm deps? -> Lambda@Edge (origin)
Already all-in on CloudFront + IAM? -> Lambda@Edge
Lowest tail latency, no cold start? -> Workers
Run logic only on cache miss? -> Lambda@Edge (origin-request)
Fast iteration + dynamic config bindings? -> Workers
Verification: confirm the routing decision
Whichever platform you choose, verify the decision from the outside and from logs. Use curl to read the header your handler stamped:
curl -sI https://app.example.com/beta/dashboard | grep -i x-route-target
# Expected:
# x-route-target: canary
curl -sI https://app.example.com/dashboard | grep -i x-route-target
# Expected:
# x-route-target: stable
On Cloudflare, stream live invocations to confirm the branch taken and catch exceptions:
wrangler tail --format pretty
# GET /beta/dashboard - Ok @ 12:04:51
# (log) routed -> canary
On AWS, the equivalent is CloudWatch. Find the region nearest your test POP, then read the most recent log stream:
aws logs tail "/aws/lambda/us-east-1.router-viewer-request" \
--region us-east-1 --since 5m --follow
# 2026-06-20T12:04:51 routed -> canary
If the curl header and the provider logs agree, the routing decision is live.
Troubleshooting
Lambda@Edge returns 502 after rewriting the origin. The Host header still points at the old origin, or CloudFront’s distribution does not allow origin override. Re-set request.headers["host"] to the new domainName and confirm the origin is reachable over HTTPS with a valid certificate. Check the CloudWatch log group in the serving region for the underlying error.
Lambda@Edge deploy “stuck” or routing stale code. You associated $LATEST or forgot to publish a new numbered version. Lambda@Edge requires a specific version. Publish a new version, re-associate it with the CloudFront behavior, and wait for the distribution status to return to Deployed before retesting.
Worker bundle exceeds the size limit on deploy. A large dependency is pulling in a Node polyfill. Run wrangler deploy --dry-run --outdir dist to inspect the bundle, drop filesystem or raw-socket packages, and prefer Web Crypto and fetch over Node equivalents.
Cold start latency spikes on Lambda@Edge. A heavy bundle is provisioning slowly in low-traffic regions. Shrink the package, move expensive init out of the handler body, and consider whether the logic belongs on origin-request (miss-only) instead of viewer-request (every request).
curl shows no x-route-target header. The Worker route is not bound, or the Lambda version is associated with the wrong event type. Confirm the Worker route pattern matches the hostname, and that the Lambda is wired to viewer-request (not viewer-response) on the correct CloudFront behavior. For more on attaching Workers to routes, revisit the parent topic, Cloudflare Workers Routing.
Frequently Asked Questions
Does Cloudflare Workers really have no cold start? Effectively yes — V8 isolates start in microseconds because they share a process, so there is no per-request VM provisioning. You may still pay a one-time cost to compile a large script, but it is far smaller than a Lambda micro-VM cold start.
Can I run my existing Node npm packages on Cloudflare Workers? Only those that target web-standard APIs or have a Workers-compatible build. Packages that use the filesystem, raw TCP sockets, or native addons will not run unmodified; Lambda@Edge, with a full Node runtime, is more forgiving here.
Why does my Lambda@Edge function not run in every CloudFront location? AWS replicates edge functions to a set of regional edge caches rather than every POP. Requests are routed back to those regions, which is why Workers generally place compute closer to the user for tail-latency-sensitive routing.
How do I run routing logic only on cache misses?
On Lambda@Edge, attach it to the origin-request hook, which fires only after a cache miss. On Cloudflare Workers, check the cache inside fetch and short-circuit on a hit before running the expensive path.
Which is cheaper for a high-traffic site?
It depends on cache-hit ratio and CPU time. Workers bill CPU time only and have no cold-start tax; Lambda@Edge bills wall-clock GB-seconds but can drop invocations sharply on origin-request when your hit ratio is high. Model your real traffic before deciding.
Related
- Deploying Cloudflare Workers for Dynamic Request Routing
- Vercel Edge vs Cloudflare Workers Performance Comparison
- Cloudflare Workers Routing
Back to Cloudflare Workers Routing