Skill 137 · AWS Lambda Web Functions
Subchapter 137.8
references/migration.mdMarkdown14 KBView on GitHub
Moving an existing Node.js HTTP app to Lambda Web Functions. Most of the work is deleting code: the adapter, the handler export, and the API Gateway wiring all go away.
Walk these before rewriting anything. A “no” means Web Functions is the wrong destination — see the decision gates in SKILL.md (opens in a new tab).
nodejs24.x. Runtime support can change, so confirm the supported runtimes with aws lambda-web help or the AWS docs rather than assuming this is the only one.index.js at the root of the ZIP./tmp between requests.node_modules, fits in 512 MB.@aws-sdk/* client the code imports is packaged, and none is marked external in the bundler config. Event Functions include the SDK in the runtime; Web Functions do not.aws lambda-core help or the AWS docs).The common shape: an Express or Fastify app wrapped by serverless-express,
@vendia/serverless-express, aws-lambda-fastify, or a hand-rolled event translator.
The router does not change. The wrapper does.
Before:
// handler.js
import serverlessExpress from '@vendia/serverless-express';
import app from './app.js';
export const handler = serverlessExpress({ app });After:
// index.js — the runtime loads exactly this path
import app from './app.js';
// The runtime probes this exact host:port for readiness.
const [HOST, PORT] = (process.env.AWS_LAMBDA_HTTP_ENDPOINT || '0.0.0.0:3000').split(':');
app.listen(PORT, HOST, () => console.log(`Listening on ${HOST}:${PORT}`));Steps:
package.json and delete the handler module.index.js, and add "type": "module" if you use import.app.listen(...) call above. Do not hardcode a port.let/var that a request writes to.node index.js and curl it. Local and deployed behaviour are now the same code path, which they were not under the adapter.What you can delete along the way: base64 request/response encoding, isBase64Encoded
handling, API Gateway event shape translation, and any context.callbackWaitsForEmptyEventLoop
workaround — background work now continues naturally after the response.
Two options, and the choice is about which API Gateway features you actually use.
Option A — replace the API with the endpoint. Simplest, and the reason to migrate. The
endpoint gives you a managed HTTPS domain directly. Move into the app: CORS (a middleware such
as cors), request validation, and authorization (validate the JWT or session in the app).
Choose authType deliberately rather than taking a default. Use IamAuth for
service-to-service traffic, so only SigV4-signed callers reach the endpoint. Reach for
ApplicationManaged only when the endpoint genuinely must be public, and treat that as a
commitment rather than a convenience, because nothing authenticates in front of the app. Its own
authentication must be implemented and verified against the new endpoint before any traffic
shifts to it. A public endpoint also needs defence in depth beyond authentication: front it with
AWS WAF and set the standard security response headers (HSTS, CSP, X-Frame-Options), which
helmet handles for Express and Fastify. See iam-and-security.md (opens in a new tab).
Option B — keep API Gateway in front of the endpoint. Keep this when you depend on usage plans and API keys, Cognito or Lambda authorizers, request/response mapping, mTLS, edge WAF, or WebSocket APIs. Treat the endpoint domain as the HTTP integration target.
Either way, translate the handler as in Path 1 — the event-shape code goes away.
| API Gateway concept | Where it goes |
|---|---|
Proxy integration event → req | Native req/res; delete the translation layer |
event.requestContext.authorizer claims | Validate the token in the app, or keep API Gateway (Option B) |
| Stage variables | serviceConfig.environmentVariables on the revision |
| Stage-level canary | revisionWeights on the endpoint (at most 2 revisions, summing to 100) |
| Per-method throttling | throttleConfig.rateLimit, which is endpoint-wide rather than per-method or per-client |
| CORS configuration | Middleware in the app → domains-and-routing.md (opens in a new tab) |
| Access logs | CloudWatch Logs from the app → observability.md (opens in a new tab) |
The closest existing shape — a function already reachable over HTTPS without API Gateway.
authType here has exactly two values, ApplicationManaged and IamAuth (see iam-and-security.md (opens in a new tab)). A function URL’s NONE corresponds to the first and AWS_IAM to the second; confirm that mapping against the API reference before relying on it for an access-control decision.awslambda.streamifyResponse becomes ordinary Node streaming — write to the response as you go. See architecture-patterns.md (opens in a new tab).throttleConfig.rateLimit (RPS) or scalingConfig.maxEnvironments (see scaling-and-concurrency.md (opens in a new tab)); throttleConfig.rateLimit: 0 is the equivalent of the reservedConcurrency: 0 kill switch.The app is probably already a listening HTTP server, so the code is close. The packaging is not.
npm ci --omit=dev and zip the result.aws-lambda-microvms, or ECS/EKS.AWS_LAMBDA_HTTP_ENDPOINT within the 10 s init timeout.Plan replacements for these before you migrate, not after:
| Feature | Replacement |
|---|---|
| Reserved / provisioned concurrency | Bound scale with throttleConfig.rateLimit or scalingConfig.maxEnvironments → scaling-and-concurrency.md (opens in a new tab). throttleConfig.rateLimit: 0 is the kill switch that reservedConcurrency: 0 gives you on Event Functions |
| Lambda layers | No layers concept in the revision API. Bundle shared code into the ZIP |
| Event source mappings (SQS, Kinesis, DynamoDB Streams) | Keep an Event Function for those paths — aws-serverless. A Web Function serves HTTP only |
| Container (OCI) image deployment | codeConfig.s3Object is the only code source in BuildConfig |
| In-environment scheduled work | EventBridge Scheduler + an Event Function |
| Non-Node runtimes | Event Functions; for custom OS needs check aws-lambda-microvms |
Feature availability changes, so confirm the layers and container-image rows against
aws lambda-web create-web-function-revision help or the API reference before ruling either out.
A mixed application is normal and expected: HTTP surface on a Web Function, queue and stream consumers on Event Functions, in the same account and stack.
A new app is fine with the defaults. A migrated app is not: three things move, and each one fails silently rather than erroring.
The log group path changes. Logs go to /aws/lambda/web/{function-name}, plus a
{logGroup}.local group holding only the logs of the region that served the request. The Event
Functions path /aws/lambda/{function-name} is not created, so
aws logs filter-log-events --log-group-name /aws/lambda/{name} returns
ResourceNotFoundException. Anything scoped to the old path silently collects nothing:
/aws/lambda/web/.Resource is scoped to /aws/lambda/* widened or repointed — a policy
written for the old prefix does not match the new group.Three Event Functions environment variables are absent. AWS_LAMBDA_LOG_STREAM_NAME,
AWS_LAMBDA_LOG_GROUP_NAME and AWS_LAMBDA_FUNCTION_NAME are all unset. Migrated structured
logging or tracing code that reads any of them logs an empty value instead of failing, which is
worse than an error because it looks like it works. Read AWS_LAMBDA_HTTP_ENDPOINT for the bind
address; for correlation use the requestId the platform already injects into every log line, or
the lambda-web-request-id and lambda-web-logstream response headers.
The CLI namespace splits. aws lambda list-functions returns only Event Functions and will
not show your Web Functions — they are under aws lambda-web list-web-functions. Scripts,
runbooks and inventory tooling that enumerate functions with the lambda namespace silently
under-report after a migration.
Do not repoint DNS as the first step.
For canarying between revisions of the Web Function itself — as opposed to between old and new
stacks — use revisionWeights on the endpoint. See deployment.md (opens in a new tab).
Active and curl https://{domainName}/ returns the expected response.authType matches intent — ApplicationManaged endpoints are public.environmentVariables during the port. Keep them in AWS Secrets Manager or SSM Parameter Store (SecureString) and fetch them at runtime.ApplicationManaged endpoints sit behind CloudFront and AWS WAF for L7 protection — rate limiting, bot control, IP reputation. The managed domain has no WAF of its own.