Est.

Migrating from AWS Lambda to Edge Workers Without Rewriting Business Logic

Remove cold starts and run business logic from edge locations instead of distant regions.

Staff Writer · · 12 min read
Cover illustration for “Migrating from AWS Lambda to Edge Workers Without Rewriting Business Logic”
Developer Productivity · September 3, 2026 · 12 min read · 2,605 words

Migrating from AWS Lambda to edge Workers is primarily a scoping exercise. The execution context changes: how a request comes in, how config gets injected, what runtime APIs are on hand. The business logic, the validation rules, the transformations, the actual reason the function exists, moves across intact. Both platforms are serverless and event-driven, both take a request and hand back a response, and that shared contract makes this closer to a translation job than a demolition job. The rest is detail, and the detail is what this piece is for.

What cold starts actually cost, and why they go away at the edge

Anyone who has run a Python or Java function on Lambda knows the tax: a cold invocation can add hundreds of milliseconds before a single line of business logic runs. Node.js functions do better but still pay something on first invocation. The delay comes from the boot sequence itself: Lambda has to stand up a Firecracker microVM, boot an operating system, and load a language runtime before it even looks at the incoming event.

Workers handles startup differently. Cloudflare, which runs Workers across 330+ edge locations worldwide, executes each Worker as a V8 isolate inside a V8 process that's already running, on a machine that's already warm. There's no OS to boot and no container to schedule. The gap between a "cold" and "warm" Workers invocation shrinks into the noise of ordinary network round-trip time, small enough that users generally can't feel it.

One caveat worth stating plainly: cold starts matter most for functions invoked rarely, or APIs with thin traffic. A high-throughput Lambda function that AWS keeps warm because it's getting hit constantly doesn't lose much to cold-start latency to begin with, so migrating purely to fix that problem is a weak argument. The distribution benefit is separate and it stacks on top: serving a response from a location near the user cuts round-trip time hard compared to a single AWS region sitting behind API Gateway, regardless of cold-start behavior at all.

A widely circulated figure from a June 2026 analysis on shattered.io described a "240x cold start gap" between the platforms. Worth naming because developers will run into that number, and worth knowing what it actually measures: raw cold-boot latency, not the end-to-end experience once network time and real traffic patterns get factored in.

The three layers of a Lambda function and which one actually has to change

Diagram: Three Layers of a Lambda Function: What Changes, What Doesn't. Visualizes: Visualize the three distinct layers of a Lambda function and which layer must change during migration to Workers.

Strip down a typical Lambda function and three layers fall out. There's the handler scaffolding: the exports.handler = async (event, context) => signature, the event parsing, the response envelope. There are the runtime dependencies: environment variables, SDK clients, npm packages that assume Node.js globals are just sitting there waiting. And there's the business logic itself: validation, data transformation, routing decisions, the shape of the response.

Layer one has to change outright. It's the Lambda-specific entry point, and it doesn't exist on Workers in any recognizable form. Layer two might need adjustment: some packages run unchanged, some need a compatibility flag, a small number have no answer at all. Layer three, the actual logic, travels intact. That's the whole structural reason this migration is tractable rather than a rewrite wearing a migration costume.

This framing scopes the project before a line of code moves, and it should decide the timeline, not the other way around. If the business logic layer is thick and the scaffolding and dependency layers are thin, do the migration and do it fast. If layer two is laced through with AWS SDK calls to a dozen different services, budget for that separately. It's a distinct piece of work with its own risk profile, and pretending otherwise is exactly how migrations blow their timelines.

Replacing the Lambda handler with the Fetch API event model

Lambda's handler signature takes an event object, shaped differently depending on whether the trigger is API Gateway v1, API Gateway v2, an Application Load Balancer, or Lambda@Edge, plus a context object. It returns a plain object with a statusCode, headers, and a body string that the platform serializes into an HTTP response.

Workers uses a model closer to what runs in a browser. A Worker exports a fetch handler that receives a standard Web Request object and must return a standard Response. It's the same interface service workers have used for years, so most of the translation work is mechanical rather than creative. event.body becomes await request.json() or await request.text(). event.headers becomes request.headers.get(...). event.queryStringParameters becomes new URL(request.url).searchParams. And the classic return { statusCode: 200, body: JSON.stringify(data) } collapses into return Response.json(data).

None of that touches the logic sitting between parsing and response construction. It's plumbing work, not surgery. The one wrinkle is Lambda@Edge, whose CloudFront-shaped event object is messier to unwrap than a standard API Gateway payload; that adds an extra translation step but doesn't change the underlying point that the logic itself travels fine. Wrangler, the CLI tool for Workers, picks up the exported fetch object automatically. There's no HTTP server to configure and no port to bind, which for anyone who has burned an afternoon debugging an Express listener that silently refused to bind on the right interface, counts as a small mercy.

Node.js compatibility: what works, what needs a flag, and what has no equivalent

The old assumption that Workers is a stripped-down browser sandbox with no filesystem and no networking is out of date. The nodejs_compat flag is now the recommended way to run most Node.js-shaped code, and it covers more ground than developers who last checked a few years ago probably expect.

Turning it on brings in node:buffer, node:crypto, node:events, node:stream, node:util, and a growing subset of node:net. That's the substrate most general-purpose npm packages actually sit on, so a lot of "will this even run on Workers" anxiety turns out to be misplaced. What's deliberately missing is node:fs. There is no local filesystem at the edge, by design; the replacements are Workers KV and R2, and they demand a different mental model, key-value lookups and object storage instead of path-based file reads.

Triaging npm dependencies follows a predictable pattern. Pure logic packages, validators, parsers, date libraries, schema libraries, almost always work without touching a line. HTTP client libraries mostly work, though a few older ones need to be swapped for something built on fetch natively. The AWS SDK v3 can run inside a Worker to reach AWS services directly, but it adds a network hop back to an AWS region, so the real question is whether that dependency earns its keep or whether it's dead weight from the old architecture. Packages that assume a persistent process, raw TCP sockets, or filesystem access need outright replacement; no flag saves those.

Week one of a migration is usually when the incompatible packages surface, and finding them then beats finding them three weeks in during integration testing. Node.js compatibility took a real step forward in September 2025, when support for node:http client and server APIs expanded significantly, shrinking the pile of packages that simply refuse to run.

Using Hono to keep business logic genuinely portable across both runtimes

Hono is a web framework built around the same standard Request, Response, Headers, and URL objects that Workers uses natively, and that shared vocabulary is what makes it useful for this migration specifically, not just another framework preference. Route handlers get written once. A thin runtime adapter, six to ten lines, wires that logic into whichever platform is running it.

On Workers, the adapter is an exported object with a fetch property that delegates to app.fetch. On Lambda, Hono ships a Lambda adapter that wraps the same app instance behind a different entry file. The upshot: one source tree can produce a Node.js Docker image, a Workers script, and a Lambda deployment package, with the business logic sitting untouched at the center of all three.

Hono doesn't fix everything, and it shouldn't get credit for what it can't touch. Express middleware built around a stateful, single-process Node.js environment, in-memory caches, setInterval-driven schedulers, raw TCP servers, needs architectural rethinking no matter what framework sits on top. That's a fact about distributed, ephemeral compute that no adapter papers over. Hono itself isn't a science project either; it runs in production at Deno, Clerk, Unkey, and other infrastructure-facing teams, a reasonable signal that it's stable enough to build a migration around. For teams coming from Express, map the route handlers to Hono's equivalents first, since the syntax is close enough to be mechanical, and only swap the entry point after. That first step is where the real migration work happens.

Environment bindings: replacing Lambda environment variables and IAM with Workers bindings

Lambda leans on two mechanisms for configuration and access: process.env for plain values, and IAM roles attached to the function's execution context for everything that touches another AWS service. Workers replaces both with typed bindings declared in wrangler.toml and injected into the handler through an env parameter. process.env technically exists on Workers, but bindings are the idiomatic path, and treating process.env as the primary tool is a habit worth dropping early.

The binding types map fairly directly onto what they replace. Plain vars and secrets bindings do what process.env used to do for config and API keys. A KV namespace binding takes over from DynamoDB or ElastiCache for simple key-value state. An R2 bucket binding replaces S3 SDK calls for object storage. A D1 binding stands in for RDS or DynamoDB when the data can live at the edge. Service bindings replace the internal HTTP calls that used to connect one Lambda function to another.

Some AWS services simply can't be replaced, and that's fine: Workers can call AWS APIs directly over the network using the AWS SDK v3 or raw HTTP with SigV4 signing. The function still works. It now carries a network hop back to an AWS region on every call, and that latency cost needs to get measured against the use case, not assumed away. The env object arrives as the second argument to the fetch handler, keeping binding setup out of the business logic entirely, a cleaner seam than Lambda's IAM model ever offered. For local development, Miniflare replicates binding injection on a developer's machine, so testing binding access doesn't require an actual deploy.

What cannot be migrated cleanly, and when the hybrid pattern is the right answer

Some functions don't belong at the edge, no matter how much compatibility work goes into them, and forcing them there anyway is the single most common way these migrations go wrong. Long-running batch jobs, ML inference workloads, ETL pipelines: Lambda's 15-minute execution ceiling and headroom up to 10 GB of memory make it the right tool for these. Functions wired deep into a VPC, direct database connections over private networking, internal calls to other services inside a security group, carry an edge-to-VPC path that usually costs more complexity than the migration saves. Anything needing GPU access or compiled dependencies that can't be expressed as WebAssembly is off the table, period.

The right move is to route functions through different doors depending on their shape. Workers takes the hot path: authentication checks, routing, caching, rate limiting, header rewriting, the things that run on every single request. Lambda sits behind it, handling workloads that need longer execution windows or deep AWS-native service access. Production teams run this split on purpose, Workers as the edge layer, Lambda as the regional compute tier behind it, and that's the correct architecture for a system with genuinely different workload shapes.

The heuristic applies function by function: if the response is cacheable, if the latency is user-facing, or if it runs on every request, it belongs at the edge. If it runs occasionally, touches large volumes of data, or needs an AWS-native service with no edge equivalent, Lambda stays exactly where it is. For a Node.js API in the 50 to 150 endpoint range, a full migration typically runs several weeks of focused engineering, and the riskiest stretch is week one, when npm compatibility problems surface all at once. Planning for that triage phase up front is the difference between a predictable timeline and a string of unpleasant surprises in week two.

How CPU-time billing changes the economics of migrated functions

Diagram: CPU-Time vs. Wall-Clock Billing: Where the Cost Difference Lives. Visualizes: Illustrate the billing gap between Lambda and Workers for a representative API function.

Lambda bills for duration, and duration includes idle time. A function that spends 200 milliseconds waiting on a database response gets billed for all 200 milliseconds, even though it's doing nothing but waiting around. Workers bills CPU time only. That same function, if it burns 15 milliseconds of actual CPU while waiting 200 milliseconds for the database, gets charged for 15 milliseconds. For most API functions, which spend the bulk of their wall-clock time waiting on I/O rather than computing anything, that's a real shift in the cost structure, not a rounding error.

At a scale of a billion requests a month, that difference compounds into something material, per a June 2026 analysis from tech-insider.org, and the gap widens further for functions with a high ratio of I/O wait to CPU use. There's a second asymmetry worth flagging: AWS charges for data transferred out of Lambda, and Workers doesn't charge egress at all. That's a line item that scales with response payload size and traffic volume, and it's easy to miss until the invoice shows up.

None of this makes Workers free. A production app pairing Workers with KV, D1, and R2 storage carries per-service billing on top of the request cost, and that full picture, not just the headline request rate, is what actually needs modeling before anyone commits to a number. The CPU-time model also changes what optimization means in practice: on Workers, cutting unnecessary computation moves the bill directly. On Lambda, the harder problem, cutting I/O wait time, is what actually moves the needle, and that's a much less tractable thing to chase.

Running the migration: a practical sequence from audit to production cutover

Audit before touching a line of code. Inventory every Lambda function: handler type (API Gateway v1 or v2, ALB, Lambda@Edge), runtime language, npm dependency list, and every AWS service call it makes. Run each one through the three-layer model and ask how thick the business logic is relative to the runtime-dependency layer wrapped around it. Anything with deep AWS entanglement, VPC connections, heavy IAM-scoped service calls, gets flagged for the hybrid pattern instead of a full migration. Forcing it across anyway is how a six-week migration turns into a six-month one.

Dependency triage comes next, and it needs to happen before any handler code gets touched, not after. Run the nodejs_compat flag against the dependency list and sort packages into three piles: works unchanged, needs a fetch-native replacement, has no answer and needs a rewrite. This is the phase that surfaces the real timeline, because a function with thin business logic and three incompatible packages takes longer to migrate than a function with thick business logic and zero dependency problems. Scoping this honestly up front saves the argument later about why the "simple" migration took a month.

From there, the handler translation and Hono adapter work described earlier turns largely mechanical, and bindings replace environment variables and IAM roles function by function. Cutover should run function by function, or endpoint by endpoint, never all at once. Keep the Lambda version live behind a feature flag or traffic split until the Workers version has run in production long enough to prove out latency and error rates under real traffic, not synthetic load tests. The functions flagged for the hybrid pattern during the audit stay on Lambda indefinitely, and that's the intended outcome of doing the audit honestly in the first place.

Sources

  1. digitalsanctuary.com
  2. zeonedge.com
  3. shattered.io
  4. 5ly.co
  5. pkglog.com

More in Developer Productivity