AWS Lambda Web Functions: Deploy Express, Hono, Fastify or Next.js With One Command, the Real Cost vs Classic Lambda, Canary Deploys and the Traps
AWS Lambda Web Functions, launched on October 12, 2026, let you run an ordinary Node.js web server (Express, Hono, Fastify or Next.js) on Lambda with no handler and no adapter. Your server listens on the host and port that Lambda puts in AWS_LAMBDA_HTTP_ENDPOINT, and one command, aws lambda-web deploy --name my-web-app --code ./my-app, gives you a live HTTPS URL. Each execution environment serves up to 128 requests at the same time (64 by default), and you pay $0.0000276944 per active vCPU-second, $0.0000036667 per GB-second of memory and $0.30 per million requests in US East on Arm. The surprise sits in the four published price examples: run all four through classic Lambda at 512 MB and the old model comes out cheaper in three of them. Web Functions win big when requests spend most of their time waiting, like a streaming AI answer: $33.94 a month against $101.00.
Ethan, a developer, runs the booking site for his friend Jake's phone repair shop. It's a small Express app on classic Lambda behind API Gateway, with a wrapper library that turns every web request into a Lambda event and back again. The day AWS announced Web Functions, he deleted the wrapper, ran one deploy command and had the site answering on a new URL within a minute. Then his local copy crashed with a TypeError, his cost estimate didn't match the launch post, and he noticed the new URL answered anyone who found it. This page is everything he worked out, on Windows 11, Mac and Kali Linux.
New to Lambda? Our plain-English guide to AWS Lambda explains functions, events and cold starts in a few minutes. Every step below starts from zero, and you can follow it without ever having written a Lambda handler.
What Lambda Web Functions are (and Lambda's three new names)
For eleven years, a Lambda function has been a handler: a function that receives an event, does some work and returns a result. To put a website on it, you needed API Gateway or a function URL in front, plus code that translated an HTTP request into an event and the result back into an HTTP response. Most people used a wrapper library for that translation, and every framework needed its own.
Web Functions drop the translation. Your code is a normal web server, the same one you start with node index.js on your laptop. Lambda starts it, sends real HTTP requests to the port it listens on, and returns its responses to the caller. No handler, no adapter, no event objects.
With this launch, AWS split Lambda into three "form factors" that share the same Firecracker isolation underneath. The functions you already have didn't change; they just got a new name, Event Functions.
| Event Functions (classic Lambda) | Web Functions (new) | MicroVMs | |
|---|---|---|---|
| Your code is | A handler that gets an event and returns a result | An HTTP server listening on a port | A whole app built from a Dockerfile, in its own virtual machine |
| Requests per environment | One at a time | Up to 128 at once (64 by default) | Whatever your app handles |
| You pay for | Requests, plus memory for every millisecond each request runs | Requests, active CPU time, and memory for as long as each environment exists | Baseline compute per second while it runs, plus snapshot storage |
| Languages | Python, Node.js, Java, .NET and the other Lambda runtimes | Node.js 24 only | Anything your Dockerfile installs |
| Best for | Queues, S3 uploads, schedules, events from other AWS services | Websites, APIs, server-side rendering, streaming responses | Running untrusted or AI-written code, dev environments |
Two details in that table change how you write code. First, one environment now serves many requests at once, so your server shares memory, global variables and the /tmp folder between users. Second, the bill counts CPU time, not waiting time. A request that sits for two seconds waiting on a database or an AI model costs almost nothing in CPU. That's the whole pitch, and it's also where both of the big surprises on this page come from.
Jake: "So it's the same Lambda with a new name?"
Ethan: "Same building, different counter. The old Lambda gave every customer their own counter and charged me the whole time they stood there deciding. The new one lets one counter serve dozens of customers at once and only charges me while I'm actually fixing a phone. The counter's rent is still extra."
Before you start on Windows 11, Mac and Kali Linux
You need three things: an AWS CLI new enough to have the lambda-web commands, Node.js 24 to run your app locally, and AWS credentials that are allowed to create a few resources. Ten minutes, start to finish.
1. AWS CLI 2.38.0 or later
The aws lambda-web commands exist in AWS CLI 2.38.0. Check yours with aws --version, then run aws lambda-web help. If the help page opens, you're set. If the CLI says lambda-web isn't a valid choice, install the latest version over the old one:
On Windows 11, in PowerShell or Command Prompt:
msiexec.exe /i https://awscli.amazonaws.com/AWSCLIV2.msi
On Mac, in Terminal:
curl "https://awscli.amazonaws.com/AWSCLIV2.pkg" -o "AWSCLIV2.pkg"
sudo installer -pkg ./AWSCLIV2.pkg -target /
On Kali Linux (or any 64-bit Intel or AMD Linux):
curl "https://awscli.amazonaws.com/awscli-exe-linux-x86_64.zip" -o "awscliv2.zip"
unzip awscliv2.zip
sudo ./aws/install --update
Open a new terminal afterward and check aws --version again. The --update flag matters on Linux: without it, the installer stops when it finds an existing copy.
2. Node.js 24 for local runs
Web Functions run your code on Node.js 24 (runtime nodejs24.x), so use Node.js 24 on your machine too. That way, anything that works locally behaves the same once it's deployed. On Windows 11 and Mac, the installer from nodejs.org is the simplest route; pick the 24.x line. On Kali Linux, Mac or Windows with WSL, a version manager such as nvm gives you exactly the version you want:
nvm install 24
nvm use 24
node --version
The deploy command doesn't need Node.js on your machine at all; it only uploads files. Node.js is for running and testing your server before you ship it.
3. Credentials and permissions
The deploy command calls AWS on your behalf, so the CLI needs credentials. If you sign in to AWS through IAM Identity Center, run aws configure sso once and aws sso login each day; on a personal account, aws login signs you in through the browser. Either way, check with:
aws sts get-caller-identity
If that prints your account ID, the deploy can find it too. If it prints an error, fix that first; our guide to "Unable to locate credentials" walks through every cause.
The identity you deploy with needs permission to manage Web Functions, to pass the execution role to Lambda (iam:PassRole), and to use the deployment bucket: sts:GetCallerIdentity, s3:ListBucket, s3:PutObject, s3:GetObject and s3:GetObjectVersion. On the very first deploy, the command also creates a role and a bucket for you, which takes iam:GetRole, iam:CreateRole, iam:PutRolePolicy, s3:CreateBucket, s3:PutBucketPolicy and s3:PutBucketVersioning. An administrator account has all of these already.
Deploy your first Web Function in one command
Start with AWS's starter app, so you know your account and CLI work before you bring your own code. Web Functions exist in only five Regions at launch, so pick one of them with --region if your CLI's default is somewhere else, such as Mumbai or Sydney:
aws lambda-web deploy --name my-web-app --hello-world --region us-east-1
That one command does six things. It writes a small TypeScript starter into a new ./my-web-app folder: server.ts, package.json, a README.md and an AGENTS.md with instructions for AI coding agents. Then it creates (or reuses) an S3 bucket with versioning turned on, named awscli-lambdaweb-<account-id>-<region>-an, and an IAM execution role named awscli-lambdaweb-my-web-app that can write logs. It uploads the code, creates the Web Function with its first revision, creates an endpoint named dev, and waits until it's live. The output is two lines:
https://a1b2c3d4e5.x9y.lambda-web.us-east-1.on.aws
Console: https://us-east-1.console.aws.amazon.com/lambda/home#/web-functions/my-web-app
Copy the first line and call it:
curl https://a1b2c3d4e5.x9y.lambda-web.us-east-1.on.aws/
The starter answers with {"message":"Hello from Lambda Web!","path":"/"}. Your domain will be different; the one above is a placeholder.
One order-of-events detail saves confusion. The command checks your identity with AWS STS before it writes anything. Run it without working credentials and it stops at once with Error: Unable to retrieve the AWS account ID with AWS Security Token Service (AWS STS) GetCallerIdentity. Verify your credentials and sts:GetCallerIdentity permission. and the my-web-app folder is not created. So if you were hoping to look at the starter files first, sign in first.
Prefer clicking? In the Lambda console, choose Web Functions, then Create Web Function. Keep the Hello World template, set the endpoint type to Home-Region, choose an authentication type (the next sections explain the choice) and keep Latest revision for auto deployment. The console creates the same role, bucket, revision and endpoint. The endpoint's domain is on the function's Endpoints tab.
| What deploy sets when you don't say | Default | Flag to change it |
|---|---|---|
| Runtime | nodejs24.x (the only one at launch) | --runtime |
| Endpoint name | dev | --endpoint-name |
| Endpoint type | HomeRegion (one Region) | --endpoint-type, fixed once created |
| Who can call it | ApplicationManaged: anyone with the URL | --auth-type IamAuth |
| New code goes live | Automatically (LatestRevision) on a HomeRegion endpoint | --auto-deployment-mode Disabled |
| Request timeout | 30 seconds (allowed: 3 to 900) | --timeout-seconds |
| Requests per environment | 64 (allowed: 1 to 128) | --max-concurrency-per-environment |
Notice the fourth row. The default endpoint is open to the internet. For a hello-world page that's fine; for anything with a database behind it, read the section on locking it down before you share the URL.
Deploy your own Express, Hono, Fastify or Next.js app
Your app needs exactly one change: it must listen on the host and port that Lambda gives it in the AWS_LAMBDA_HTTP_ENDPOINT environment variable, written as host:port. Lambda picks the address. If your server listens anywhere else, such as a hard-coded port 3000, requests cannot reach it.
Here's an Express server written so it works both on Lambda and on your own machine. The fallback after || only applies when the variable isn't set, which happens only locally:
import express from "express";
const app = express();
const [host, port] = (process.env.AWS_LAMBDA_HTTP_ENDPOINT || "127.0.0.1:3000").split(":");
app.get("/", (req, res) => {
res.json({ message: "Hello from Lambda Web Functions!" });
});
app.listen(port, host, () => {
console.log(`Server running on port ${port}`);
});
Add "type": "module" to package.json for the import syntax, or use require instead. The same idea works in every framework:
// Hono, with @hono/node-server
import { Hono } from "hono";
import { serve } from "@hono/node-server";
const app = new Hono();
app.get("/", (c) => c.json({ ok: true }));
const [host, port] = (process.env.AWS_LAMBDA_HTTP_ENDPOINT || "127.0.0.1:3000").split(":");
serve({ fetch: app.fetch, hostname: host, port: Number(port) });
// Fastify
import Fastify from "fastify";
const app = Fastify();
app.get("/", async () => ({ ok: true }));
const [host, port] = (process.env.AWS_LAMBDA_HTTP_ENDPOINT || "127.0.0.1:3000").split(":");
await app.listen({ host, port: Number(port) });
Next.js needs one extra step, because its standalone server reads its own variables, HOSTNAME and PORT, for the listen address. Build with output: "standalone" in next.config.js, then make the entry file an index.js that copies Lambda's address into those variables before the server starts:
// index.js, next to the standalone server.js
const [host, port] = (process.env.AWS_LAMBDA_HTTP_ENDPOINT || "127.0.0.1:3000").split(":");
process.env.HOSTNAME = host;
process.env.PORT = port;
await import("./server.js");
Which file Lambda starts
By default, the runtime loads index.js at the root of your package. If your entry file has another name, the deploy command looks at main and exports in package.json, then at common names such as server.js, and adds a small index.js wrapper that imports the file it found. You can name it yourself with --entry-point dist/server.js. The other way, setting AWS_LAMBDA_NODEJS_ENTRYPOINT with --env, makes the runtime load that file directly. Use one or the other, not both.
Deploy it
cd my-app
npm ci --omit=dev
cd ..
aws lambda-web deploy --name my-web-app --code ./my-app
The first time, the command asks you to press Enter before it creates the Web Function. In a script or a CI/CD pipeline, there's nobody to press Enter, so add --create. Every later run with the same --name uploads a new revision, waits for it to become Active, and (on the default dev endpoint) moves traffic to it. The URL stays the same.
What gets uploaded, and the .env file that doesn't
The command zips the folder you point it at, node_modules included, so install your production dependencies before you deploy. It leaves out a list of files on purpose, and one of them catches almost everyone: .env and every .env.* file are left behind every time. Neither are .npmrc, .netrc, *.pem, *.key, id_rsa*, .git/, .aws/, .ssh/ or node_modules/.cache/. Hidden files and folders in general stay behind unless you add --include-hidden-files, except for the build folders frameworks need: .next/, .nuxt/, .svelte-kit/, .output/, .prisma/ and .yarn/ always go up.
So an app that reads its settings with dotenv starts fine on your laptop and finds every setting undefined on Lambda. Pass plain settings with --env, once per variable:
aws lambda-web deploy --name my-web-app --code ./my-app \
--env APP_ENV=production --env LOG_FORMAT=json
On later deploys, --env adds or replaces only the keys you name and keeps the rest; --unset-env NAME removes one. For passwords and API keys, skip environment variables altogether and load them at runtime from AWS Secrets Manager, so they stay out of your deployment package and your shell history.
Native modules must be built for Arm64
Web Functions run on Arm64 (Graviton) only. Pure JavaScript doesn't care, but packages with compiled parts, such as sharp, bcrypt or some database drivers, ship a binary for one processor type. If you run npm ci on an Intel or AMD machine, you upload x86_64 binaries that can't load on Arm. Install dependencies for Linux on Arm64, for example inside an Arm64 container or a CI runner on Graviton, and check the module's docs for its own flags. Our guide to Lambda's "invalid ELF header" error shows what the mismatch looks like.
Run it on your own machine first (and the crash in the launch sample)
The launch post's sample server reads the address like this:
const [host, port] = process.env.AWS_LAMBDA_HTTP_ENDPOINT.split(':');
On Lambda, that variable always exists. On your machine, it doesn't, because Lambda isn't there to set it. Run that sample with a plain node index.js on Node.js 24.21 and it stops on line 4 before Express even loads its routes:
TypeError: Cannot read properties of undefined (reading 'split')
at file:///.../index.js:4:59
You have two fixes. Use the fallback version from the section above, or set the variable yourself before you start the server:
# Mac and Kali Linux
AWS_LAMBDA_HTTP_ENDPOINT=127.0.0.1:3000 node index.js
# Windows 11, PowerShell
$env:AWS_LAMBDA_HTTP_ENDPOINT="127.0.0.1:3000"; node index.js
# Windows 11, Command Prompt
set AWS_LAMBDA_HTTP_ENDPOINT=127.0.0.1:3000
node index.js
Then open http://127.0.0.1:3000/ in your browser, or call it with curl. The sample answers 200 with {"message":"Hello from Lambda Web Functions!"}.
There's a quieter trap one step away. Set the variable to only a port, AWS_LAMBDA_HTTP_ENDPOINT=3000, and nothing crashes. The split puts 3000 into host and leaves port empty, Express picks a random free port, and the log proudly says Server running on port undefined. If you ever see that line, you've left out the host: the value must be host:port, colon included.
The starter from --hello-world handles this for you: it listens on port 3000 locally, and LOCAL_PORT changes that if 3000 is busy.
The 10-second rule
When Lambda starts a new environment, your server must begin listening within 10 seconds. Lambda sends it requests as soon as it listens, not before. If your app loads a large file, warms a cache or connects to five services at startup, start listening first and do the heavy work afterward. Make the routes that need it wait on a shared promise, so ten requests arriving at once don't each start their own copy of the slow work. The same habit shortens cold starts, which still happen when a Web Function scales up from zero.
The default URL is public: lock it down before you share it
Every Web Function endpoint has one of two authentication types, and the deploy command picks the open one unless you say otherwise.
| Auth type | Who can call it | Use it for |
|---|---|---|
ApplicationManaged (default) | Anyone who knows the domain. Lambda checks nothing and forwards every request to your app. | Public websites, and apps that check their own logins (OAuth, cookies, JWT) |
IamAuth | Only callers who sign each request with AWS credentials that are allowed to invoke this endpoint | Internal APIs, service-to-service calls, and anything behind CloudFront |
Two facts make the default riskier than it looks. On an ApplicationManaged endpoint, Lambda doesn't evaluate the function's resource-based policy for invoke requests at all, so you can't use a policy to narrow who calls it. And no Web Function domain is private at the network level, whatever its auth type: there's no PrivateLink option, and the domain resolves from anywhere on the internet. Your security is the auth type plus whatever your app checks.
Jake: "Wait, so anybody could open the booking admin page?"
Ethan: "Anybody who found the address, yes. It's like leaving the back door unlocked on a street nobody walks down. Probably fine, until the one night it isn't. I'm putting a lock on it today."
Option 1: IamAuth for callers that have AWS credentials
Create the endpoint with IAM checking switched on:
aws lambda-web deploy --name my-web-app --code ./my-app --auth-type IamAuth
Callers in the same AWS account need lambda:InvokeWebFunctionEndpoint on the endpoint's ARN in their own IAM policy:
{
"Version": "2012-10-17",
"Statement": [{
"Effect": "Allow",
"Action": "lambda:InvokeWebFunctionEndpoint",
"Resource": "arn:aws:lambda:us-east-1:111122223333:web-function/my-web-app/endpoint/dev"
}]
}
Callers in another account need that policy plus a statement in the Web Function's resource-based policy that allows them, added with aws lambda-web put-resource-policy.
Every request must then be signed with AWS Signature Version 4 using the service name lambda, and it must carry an X-Amz-Content-SHA256 header, even with no body. Without that header, Lambda answers 403 Forbidden. Here's the catch for quick tests: curl's built-in AWS signing, --aws-sigv4, doesn't add that header for the lambda service on its own. Add it yourself and curl signs it along with the rest:
curl --aws-sigv4 "aws:amz:us-east-1:lambda" \
--user "$AWS_ACCESS_KEY_ID:$AWS_SECRET_ACCESS_KEY" \
-H "x-amz-security-token: $AWS_SESSION_TOKEN" \
-H "x-amz-content-sha256: UNSIGNED-PAYLOAD" \
https://a1b2c3d4e5.x9y.lambda-web.us-east-1.on.aws/
Leave out the x-amz-security-token line if you use long-term access keys. UNSIGNED-PAYLOAD is the literal value AWS accepts when you don't sign the body. Lambda doesn't read the body when it checks the signature, so it has nothing to compare. After a signature passes, Lambda removes the Authorization header before your app sees the request. MultiRegion endpoints use the newer Signature Version 4a instead, which signs for a set of Regions.
Option 2: CloudFront in front, with origin access control
For a public website that should only be reachable through your own domain, put Amazon CloudFront in front and let it sign every request to Lambda for you. CloudFront added Web Functions as an origin type on launch day. The endpoint must use IamAuth; create the origin access control with origin type lambda and signing behavior always-amz-auth (CloudFront rejects the other signing behaviors for this origin). Then allow your distribution, and only your distribution, in the Web Function's resource-based policy:
{
"Version": "2012-10-17",
"Statement": [{
"Sid": "AllowCloudFrontOACAccess",
"Effect": "Allow",
"Principal": { "Service": "cloudfront.amazonaws.com" },
"Action": "lambda:InvokeWebFunctionEndpoint",
"Resource": "arn:aws:lambda:us-east-1:111122223333:web-function/my-web-app/endpoint/prod",
"Condition": { "StringEquals": {
"aws:SourceArn": "arn:aws:cloudfront::111122223333:distribution/<distribution-id>"
} }
}]
}
aws lambda-web put-resource-policy \
--resource-arn "arn:aws:lambda:us-east-1:111122223333:web-function/my-web-app" \
--policy file://policy.json --region us-east-1
Note that the policy attaches to the function's ARN, not the endpoint's. A function has exactly one resource-based policy, and put-resource-policy replaces it, so to add a statement later, read the current one with get-resource-policy, add yours, and pass the returned revisionId with --revision-id. If someone changed it in the meantime, you get a ConflictException instead of silently wiping their change.
A pleasant side effect: CloudFront doesn't use the Authorization header for its own signature, and Lambda strips only values that are AWS signatures (starting with AWS4-). So your app's own login tokens and cookies still arrive. Forward the Authorization header in your CloudFront settings and they work as before.
If you use ApplicationManaged behind CloudFront instead, the endpoint stays public at its own address, and anyone who finds it skips CloudFront and AWS WAF entirely. In that setup, have CloudFront add a secret header and reject requests in your app that don't carry it.
For teams: stop anyone creating a public endpoint
If you run an AWS Organization, one service control policy blocks the open default everywhere. It denies lambda:CreateWebFunction, lambda:CreateWebFunctionEndpoint and lambda:UpdateWebFunctionEndpoint whenever lambda:Request/WebFunctionAuthType equals ApplicationManaged. With it in place, a plain aws lambda-web deploy fails with access denied until the developer adds --auth-type IamAuth, which is exactly the nudge you want.
What Web Functions cost, and when classic Lambda is cheaper
Here are the published prices for US East (N. Virginia), the Region AWS uses for all its examples. Web Functions run on Arm only, so the fair comparison is classic Lambda on Arm too.
| Price (US East, Arm) | Web Functions | Event Functions (classic) |
|---|---|---|
| Requests | $0.30 per million | $0.20 per million |
| Compute | $0.0000276944 per active vCPU-second | $0.0000133334 per GB-second of configured memory, for the whole request |
| Memory | $0.0000036667 per GB-second, for as long as each environment exists | Included in the line above |
| Free tier | Left out of the published examples | 1 million requests and 400,000 GB-seconds a month |
The difference in the "Compute" row is the whole story. Classic Lambda charges for every millisecond a request is open, including the time it spends waiting on a database. Web Functions charge CPU only while your code is actually working, billed in 1 ms steps. But they also charge memory for the environment's entire life, including quiet gaps between requests, and the request price is 50% higher.
The pricing page works through four example apps, each assuming steady traffic and exactly one environment running all month (2,592,000 seconds). Here are their Web Function totals next to the same traffic on a classic Arm function set to 512 MB, using the same request durations:
| Example app | Requests / month | Time per request (CPU part) | Web Functions | Classic Lambda, 512 MB |
|---|---|---|---|---|
| Server-rendered website | 15 million | 150 ms (40 ms) | $25.78 | $18.00 |
| API waiting on a database | 10 million | 200 ms (50 ms) | $19.61 | $15.33 |
| Login check on every request | 100 million | 50 ms (12 ms) | $64.56 | $53.33 |
| Streaming AI answers | 5 million | 3,000 ms (200 ms) | $33.94 | $101.00 |
The classic column is simple arithmetic you can check. For the website: 15 million requests × 0.15 seconds × 0.5 GB = 1,125,000 GB-seconds × $0.0000133334 = $15.00, plus 15 × $0.20 for requests = $18.00. For streaming: 5 million × 3 seconds × 0.5 GB = 7,500,000 GB-seconds = $100.00, plus $1.00 for requests = $101.00.
So the rule of thumb is about the share of each request your code spends on the CPU. A classic Arm function at 512 MB costs $0.0000066667 for each second a request is open. A Web Function costs $0.0000276944 for each second of CPU. Divide one by the other and you get about 0.24. Web Functions start to win when CPU is less than roughly a quarter of each request's total time, compared with a 512 MB classic function. Against a 1,024 MB classic function, the line moves to about half. The memory charge and the higher request price pull it lower, and many concurrent requests sharing one environment push it back up.
Be fair to both sides when you run your own numbers. A classic function at 512 MB gets only a fraction of a vCPU, so CPU-heavy requests run longer there than the durations above assume, which narrows the first three gaps. On the other side, those examples keep one environment alive all month. A site with a few hundred visits a day lets its environment shut down between visits, and classic Lambda's free tier covers a small site completely. Pull your real numbers (request count, average duration and the CPU share from your logs or a profiler) before you move a busy app for the price alone.
Two smaller items land on the same bill. Data transfer is charged at standard AWS rates, including traffic between your Web Function and your VPC. And logs cost the normal CloudWatch Logs price, while Lambda's own Web Function metrics are free. A chatty app can spend more on logs than on compute; our breakdown of CloudWatch Logs ingestion costs shows how to keep that in check.
Jake: "So is it cheaper for my shop or not?"
Ethan: "For the booking page, it's about the same; it's quick work and not much waiting. For the chat helper that waits three seconds on the AI for every answer, it's a third of the price. I'm moving the chat helper first."
One environment, many requests: the trap in code moved from classic Lambda
Classic Lambda gave each request its own environment. Your code could be sloppy about shared state, block the CPU for a second, or leave temp files lying around, and only that one request ever noticed. Web Functions put up to 64 requests (by default) into the same Node.js process. Node.js runs your JavaScript on a single thread: while one request waits on a database, the others run. While one request keeps the CPU busy, nothing else runs at all.
Here's how big that effect is. Picture an Express server with three routes: /fast returns at once, /wait awaits a 2-second timer, and /block runs a 2-second busy loop. Fire one slow request, then five /fast requests alongside it, on Node.js 24.21:
| While this runs | Five /fast requests took |
|---|---|
/wait (2 s of waiting, like a database call) | 32 to 35 ms each |
/block (2 s of CPU work) | 1,957 to 1,962 ms each |
One CPU-heavy request made every other request in the environment about 60 times slower. On classic Lambda that same route only ever slowed itself down. So before you move an app across, look for synchronous work hiding in request handlers: JSON.parse on a huge body, image resizing, password hashing with a synchronous call, fs.readFileSync, big loops over arrays. Move it to worker_threads, make it asynchronous, or lower the concurrency so fewer requests share the stall:
aws lambda-web deploy --name my-web-app --code ./my-app --max-concurrency-per-environment 16
Lambda also sets its own limit for each environment, adjusted to resource use, which stays at or below yours. The most an endpoint can handle in one Region is maxEnvironments times that limit. For example, 20 environments × 64 requests = 1,280 requests at once, and fewer if Lambda lowers the per-environment number. maxEnvironments accepts 2 to 10,000; leave it unset and Lambda scales up to your account quotas.
Shared state: globals, /tmp and memory
Three more things change when requests share a process:
- Globals are shared. A variable that held "the current user" worked on classic Lambda because only one request existed at a time. On a Web Function, the next request overwrites it mid-flight. Keep per-request data on
req,resor your framework's context. Watch read-modify-write code on shared caches or counters that spans anawait, since another request can change the value in between. /tmpis shared too, 512 MB for every request in the environment, and files stay until the environment is removed. Give temp files unique names and delete them when you're done.- Memory is fixed at 2 GB per environment. There's no memory setting to raise. The Node.js heap defaults to 1,024 MB; set
NODE_OPTIONS=--max-old-space-size=1536with--envfor more, and leave room for buffers, native modules and the runtime itself.
The good news runs the other way. Connection pools, SDK clients and in-memory caches set up at startup now serve dozens of requests instead of one, which is a large part of why the same work can cost less here.
Updates, canary releases and rollback
A Web Function has two kinds of child resources. A revision is a frozen snapshot of your code and settings (timeout, concurrency, environment variables); changing any of them creates a new one. An endpoint is a URL that sends traffic to one revision, or splits it between two.
On the default dev endpoint, auto deployment is LatestRevision: each deploy goes live for everyone as soon as the new revision is Active. That suits development. For production, use manual weights, so a new revision gets a slice of traffic first:
aws lambda-web update-web-function-endpoint \
--function-name my-web-app \
--endpoint-name prod \
--auto-deployment-mode Disabled \
--revision-weights '[{"revisionId": "<current-revision-id>", "weight": 90}, {"revisionId": "<new-revision-id>", "weight": 10}]'
That sends 90% of requests to the current revision and 10% to the new one. Watch your errors and latency, then move to 50/50 and 0/100. Rolling back is the same command with the weights reversed. Find revision IDs with aws lambda-web list-web-function-revisions --function-name my-web-app.
Three details catch people here:
- "Active" doesn't mean "working." Lambda doesn't check that your server starts when it creates a revision or points an endpoint at it. A revision with a typo in the entry file shows Active, and every request fails. Send a test request after every deploy, and check the logs.
- The API default is the opposite of the CLI's. If you create a function with
create-web-functionand leave outautoDeploymentMode, it defaults toDisabled, and new revisions receive no traffic until you set weights. - Old revisions get cleaned up. When a Web Function goes over its revision limit, Lambda deletes the oldest revisions that no endpoint routes to. Keep your rollback revision attached to an endpoint (a
testendpoint works), and keep its code object version in S3, until you're sure.
Lambda's built-in metrics don't split by revision, so during a canary, log the revision ID with each request in your own application logs and compare the two.
Serving users worldwide: MultiRegion and PerRegion endpoints
At launch, Web Functions run in five Regions: US East (N. Virginia), US East (Ohio), US West (Oregon), Asia Pacific (Tokyo) and Europe (Ireland). An endpoint has one of three types:
| Endpoint type | What callers see | Good for |
|---|---|---|
HomeRegion | One Regional domain, one Region | Most apps, and the only type that can reach into your VPC |
MultiRegion | One global domain, <domain-id>.lambda-web.global.on.aws; each request goes to the Region with the lowest latency for that caller, on a best-effort basis | Global sites that should feel fast everywhere |
PerRegion | A separate domain in each Region; you decide how clients pick | Apps with their own DNS or failover rules |
An endpoint's type and its list of Regions are fixed the moment you create it. To go global with an existing app, create a second endpoint rather than changing the first:
aws lambda-web deploy --name my-web-app --code ./my-app \
--endpoint-name global --endpoint-type MultiRegion \
--regions us-east-1,eu-west-1,ap-northeast-1 --region us-east-1
The --region flag (or your CLI default) sets the home Region, and the list must include at least one Region besides it. Then move your users to the new domain and delete the old endpoint when traffic has drained.
Running in several Regions is real distributed-systems work, and a few rules come with it:
- MultiRegion and PerRegion endpoints use
Disabledauto deployment, so you move traffic to each new revision yourself with revision weights. - An update can succeed in one Region and fail in another. After each change, check
updateStatusfor every entry inregionalEndpoints(fromget-web-function-endpoint) before you call the deploy done. - Your database, secrets and keys must be reachable from every Region that serves traffic. A Tokyo request that calls a database in Virginia is still slow.
- VPC access (the egress network connector, below) works only with HomeRegion endpoints.
- Logs and request metrics are copied to the home Region asynchronously, usually within a few minutes, so a pipeline that checks metrics the second a new endpoint goes live should wait before it decides something failed.
What Web Functions don't do at launch
Most surprises after a move come from this list. Read it before you plan a migration:
- Node.js only.
nodejs24.xon Arm64 is the one runtime. Python, Java and .NET apps stay on Event Functions or move to MicroVMs. The runtime is patched automatically, and you can't pin a patch version. - No WebSockets. Lambda strips the upgrade headers before your server sees the request, so the handshake can't complete. For one-way live updates, server-sent events work. For real two-way sockets, use an API Gateway WebSocket API (our guide to WebSocket connection failures covers the setup traps), or MicroVMs, which accept WebSocket, HTTP/2 and gRPC traffic.
- No custom domain or AWS WAF directly on the endpoint. Put CloudFront in front for both. Our guide to custom domains with CloudFront and Route 53 covers the certificate and DNS side. One rule specific to Web Functions: don't forward the viewer's
Hostheader, because Lambda routes on it and returns 404 when it isn't the endpoint's own domain. With managed policies, chooseAllViewerExceptHostHeader. - No CORS headers. Lambda adds none. If a browser on another domain calls your API, your app (or CloudFront) must answer the
OPTIONSpreflight and send the rightAccess-Control-Allow-*headers. - HTTP/1.1 only, HTTPS only, port 443. HTTP/1.0 requests get
505. CloudFront can still offer HTTP/2 and HTTP/3 to visitors in front of it. - Seven methods. GET, HEAD, POST, PUT, PATCH, OPTIONS and DELETE. Anything else gets
400before your code runs. - 15 minutes per request at most, 30 seconds by default. The timeout covers both receiving the body and streaming the response; when it runs out, the client gets a cut-off body.
- Request first, then response. Lambda delivers the whole request body before it passes on your response, so read the body completely before you start writing.
- No automatic tracing. Lambda passes an incoming
X-Amzn-Trace-Idheader through but creates no X-Ray segments; send traces from your own code if you need them.
Jake: "That's a lot of 'no' for something brand new."
Ethan: "It's a first version. My booking site uses none of those things, so for me it's a yes. The trick is reading the list before you move, not after."
Reaching a database in your VPC
A Web Function runs on infrastructure Lambda manages, not inside your VPC. By default, its outbound traffic goes straight to public IPv4 addresses, and it has no route to anything private, such as an RDS database with no public address. To reach those, you attach an egress network connector to the revision (the egressNetworkConnectorArn setting in its service config). Traffic then leaves through network interfaces in the connector's subnets, and your route tables, security groups and network ACLs apply as usual. Read about VPCs in plain English first if subnets and security groups are new to you.
Three rules decide whether it works:
- A connector replaces the default path; it doesn't add to it. Once traffic goes through your VPC, reaching the public internet needs a route from those subnets to a NAT gateway (or an egress-only internet gateway for IPv6). Our fix for Lambda in a VPC with no internet applies here too, and so does the warning about NAT gateway bills.
- There's no fixed source IP address. Allow the connector's security groups on your database's security group, rather than an IP range.
- HomeRegion endpoints only. Lambda rejects any attempt to point a MultiRegion or PerRegion endpoint at a revision that uses a connector.
Attaching a connector doesn't make the endpoint private and doesn't change who can call it. Inbound and outbound are configured separately.
Logs, metrics and the request ID
Your app's console.log output lands in CloudWatch Logs. In the home Region, the log group is /aws/lambda/web/my-web-app, with logs from every Region the endpoint serves. Each Region also has /aws/lambda/web/my-web-app.local, which holds that Region's logs before they're copied home. Use the .local group when you're chasing a problem in one Region right now.
The execution role needs logs:CreateLogGroup, logs:CreateLogStream and logs:PutLogEvents. The role that deploy creates has them. If you bring your own role without them, your app's output is simply lost, with no error anywhere; our guide to a log group that doesn't exist covers this kind of silent gap.
Metrics live in two CloudWatch namespaces, both free: AWS/Lambda/Web for requests and errors (gathered in the home Region) and AWS/Lambda/Web/Usage for usage and throttling (kept in each Region, so alarm on them in each Region). Two deploy flags control how much you log: --application-log-level (TRACE to FATAL) and --system-log-level (DEBUG, INFO or WARN). Keep system logs at DEBUG only while you need them.
Every response carries a lambda-web-request-id header, and Lambda adds the same ID to the request your app receives. When a user reports an error, that ID finds the matching log lines. Responses can also carry lambda-web-logstream, naming the log stream of the environment that served the request, and lambda-web-error-type when Lambda itself refused or cut off a request.
Troubleshooting by symptom
TypeError: Cannot read properties of undefined (reading 'split')
You ran code that reads AWS_LAMBDA_HTTP_ENDPOINT on a machine where Lambda didn't set it. Set it yourself (AWS_LAMBDA_HTTP_ENDPOINT=127.0.0.1:3000 node index.js) or add the || "127.0.0.1:3000" fallback. Commands for each OS.
"Unable to retrieve the AWS account ID with AWS Security Token Service (AWS STS) GetCallerIdentity"
The deploy command couldn't find working credentials, or your identity can't call sts:GetCallerIdentity. Run aws sts get-caller-identity; when it prints your account, deploy works. Signed in with SSO? Run aws sso login again; sessions expire.
"Server running on port undefined"
The variable holds only a port, like 3000, so the split put it in host and left port empty. Use host:port, such as 127.0.0.1:3000.
Deploy succeeded, endpoint is Active, every request fails
Lambda doesn't check that your server starts. Look in the log group for a crash at startup: a missing dependency (did node_modules get uploaded?), a missing setting from a .env file that wasn't uploaded, a server listening on a fixed port instead of AWS_LAMBDA_HTTP_ENDPOINT, or a startup that takes longer than 10 seconds before it listens.
process.env.SOMETHING is undefined on Lambda but fine locally
Deploy skips .env or .env.* files. Pass settings with --env KEY=VALUE, and load secrets from Secrets Manager at runtime.
"invalid ELF header" or a native module that won't load
A compiled dependency was built for x86_64, and Web Functions run on Arm64. Reinstall dependencies for Linux on Arm64 and deploy again.
404 when going through CloudFront
CloudFront is forwarding the visitor's Host header, and Lambda routes on Host. Switch the origin request policy to one that leaves Host out, such as AllViewerExceptHostHeader.
403 Forbidden on an IamAuth endpoint
Check four things: the request is signed with SigV4 (SigV4a for MultiRegion) using the service name lambda; it carries X-Amz-Content-SHA256 (curl's --aws-sigv4 doesn't add it by itself); the credentials haven't expired; and a policy allows lambda:InvokeWebFunctionEndpoint on that endpoint's ARN.
505 HTTP Version Not Supported
The client sent HTTP/1.0. Web Function endpoints accept HTTP/1.1. Some old monitoring tools and load testers default to 1.0; switch them to 1.1.
431 Request Header Fields Too Large
The request line and headers together passed 128 KiB, usually from piled-up cookies or a huge token. That limit counts headers CloudFront forwards too, so forward only the cookies and headers your app needs.
400 Bad Request before your code even runs
The request used a method outside GET, HEAD, POST, PUT, PATCH, OPTIONS and DELETE, or had no Host header. Lambda rejects both before forwarding.
Every request in the app slowed down at once
One request is doing CPU-heavy synchronous work and blocking the event loop for everyone in its environment. Find it in your logs by duration, move it to worker_threads, or lower --max-concurrency-per-environment. The numbers.
The response stops halfway (Runtime.RequestTimeout)
The request hit its timeout, 30 seconds by default, while still streaming. The client keeps the status code already sent and gets a cut-off body. Raise --timeout-seconds (up to 900), and stop upstream work when the client disconnects. For more on timeouts, see Lambda's "Task timed out" error.
429 or 503 responses under load
A 429, or a lambda-web-error-type such as Platform.EndpointRequestThrottle, means the endpoint's rate limit, its environment cap or an account quota was reached. Retry 429s, and 503s with no lambda-web-error-type header, using backoff and jitter, but only for requests that are safe to repeat. Check aws lambda-web get-web-account-settings for your quotas.
No logs at all in CloudWatch
Your execution role is missing logs:CreateLogGroup, logs:CreateLogStream or logs:PutLogEvents. Add them; Lambda can't write a single line without them.
Deploy just sits there in a CI pipeline
It's waiting for you to press Enter to confirm creating the function or endpoint. Add --create for non-interactive runs.
Web Functions, classic Lambda, API Gateway or MicroVMs: which to pick
| Your situation | Pick | Why |
|---|---|---|
| Express, Hono, Fastify or Next.js app, mostly waiting on databases or APIs | Web Functions | No adapter, many requests share one environment, waiting costs almost nothing |
| Streaming answers from an AI model | Web Functions | The 3-second-wait case is where the price gap is widest |
| Short, CPU-heavy requests (resizing, rendering, hashing) | Classic Lambda | Pays only for each request's own time, and one slow request can't stall the rest |
| Python, Java or .NET API | Classic Lambda | Web Functions run Node.js only |
| Queue messages, S3 uploads, schedules | Classic Lambda | That's exactly what Event Functions are for |
| Need API keys, usage plans, request validation or WebSockets | API Gateway (with either Lambda) | Web Function endpoints have none of these built in |
| Running code written by users or by an AI agent | MicroVMs | A full VM per session, with suspend and resume |
| Tiny site, a few hundred visits a day | Classic Lambda | The free tier covers it; Web Functions' examples leave the free tier out |
If you already run Express on classic Lambda with a wrapper library and API Gateway in front, the move is small: delete the wrapper, listen on AWS_LAMBDA_HTTP_ENDPOINT, deploy, test on the new URL, then switch traffic over. Keep the old function running until the new one has handled a normal day.
Delete everything when you're done experimenting
aws lambda-web delete-web-function --function-name my-web-app
That removes the Web Function with all its endpoints and revisions, and its domain is retired for good; Lambda will not hand the same domain to anyone else. It does not remove the things deploy created around it:
- The IAM role
awscli-lambdaweb-my-web-app. Delete it in the IAM console, which removes its inline policy along with it. - The code packages under
lambda-web/my-web-app/in the bucketawscli-lambdaweb-<account-id>-<region>-an, including old versions (the bucket has versioning on, so turn on Show versions in the S3 console to see them). Other Web Functions in the same account and Region share this bucket, so delete only your function's folder. - The log groups. Run
aws logs delete-log-group --log-group-name /aws/lambda/web/my-web-app, and the same with.localon the end in each Region the endpoint used.
Leftover packages and logs cost a little each month, so clearing them keeps your bill clean. If you're new to watching AWS costs, set a budget alert first; our guide to AWS billing and budgets takes five minutes.
Frequently asked questions
What are AWS Lambda Web Functions?
A new kind of Lambda, launched October 12, 2026, that runs a normal Node.js web server instead of an event handler. Your Express, Hono, Fastify or Next.js app listens on the address in AWS_LAMBDA_HTTP_ENDPOINT, and Lambda gives it an HTTPS endpoint, scales it, and serves many requests per environment.
How do I deploy an Express app to Lambda Web Functions?
Make the server listen on the host and port in AWS_LAMBDA_HTTP_ENDPOINT, install production dependencies, then run aws lambda-web deploy --name my-web-app --code ./my-app on AWS CLI 2.38.0 or later. The command prints the HTTPS URL when the endpoint is live.
What is AWS_LAMBDA_HTTP_ENDPOINT?
An environment variable Lambda sets inside every Web Function, holding the host and port your server must listen on, as host:port. Lambda doesn't set it on your computer, so code that reads it needs a fallback, such as 127.0.0.1:3000, for local runs.
Do Lambda Web Functions support Python?
No. At launch, Web Functions support Node.js only, on the nodejs24.x runtime and Arm64 processors. Python, Java and .NET apps can keep using classic Lambda, now called Event Functions, or run in Lambda MicroVMs, which run anything a Dockerfile installs.
Which Regions support Lambda Web Functions?
Five at launch: US East (N. Virginia), US East (Ohio), US West (Oregon), Asia Pacific (Tokyo) and Europe (Ireland). If your AWS CLI defaults to another Region, add --region us-east-1, or another of the five, to your deploy command.
How much do Lambda Web Functions cost?
In US East on Arm: $0.30 per million requests, $0.0000276944 per active vCPU-second, and $0.0000036667 per GB-second of memory for as long as each environment exists, plus data transfer. The server-rendered website example, 15 million requests a month, comes to $25.78.
Are Lambda Web Functions cheaper than regular Lambda?
Only for requests that mostly wait. On the four published examples, a 512 MB classic Arm function is cheaper for the website, API and login-check cases, while Web Functions cost $33.94 against $101.00 for streaming AI answers. Roughly, Web Functions win when CPU is under a quarter of each request's time.
Do I still need the Lambda Web Adapter?
Not for Web Functions. Your app handles HTTP directly with its normal routing and middleware, so the AWS Lambda Web Adapter and other event-to-HTTP wrappers aren't needed. Classic Lambda functions behind API Gateway or a function URL still need one if you run a web framework on them.
Is a Lambda Web Function URL public?
Yes, by default. The deploy command creates an ApplicationManaged endpoint that anyone with the domain can call. Add --auth-type IamAuth to require signed requests, or put CloudFront with origin access control in front. The domain itself always stays reachable from the internet.
Can I use a custom domain with Lambda Web Functions?
Not directly. Endpoints don't support custom domain names or AWS WAF. Put an Amazon CloudFront distribution in front, attach your certificate and domain there, and set the Web Function endpoint as the origin. Leave the viewer's Host header out of what CloudFront forwards.
Do Lambda Web Functions support WebSockets?
No. Lambda removes the upgrade headers before forwarding requests, so WebSocket handshakes can't complete. Server-sent events work for one-way streaming. For two-way sockets, use an API Gateway WebSocket API or Lambda MicroVMs, which accept WebSocket connections.
How many concurrent requests can a Lambda Web Function handle?
Each execution environment handles up to 128 requests at once; the default is 64, set with --max-concurrency-per-environment. Lambda adds environments as traffic grows, up to maxEnvironments (2 to 10,000) or your account quotas, so 20 environments at 64 handle up to 1,280 requests at once.
What is the timeout for Lambda Web Functions?
30 seconds by default, adjustable from 3 to 900 seconds (15 minutes) with --timeout-seconds. The timeout covers receiving the request body and streaming the response. When it runs out, Lambda ends the response and the client gets a truncated body.
How much memory does a Lambda Web Function get?
Each execution environment gets 2 GB, with no setting to change it. The Node.js heap defaults to 1,024 MB; raise it with NODE_OPTIONS=--max-old-space-size=1536, leaving room for buffers, native modules and the runtime. You pay only for memory actually consumed.
What is the difference between Lambda Web Functions and Lambda MicroVMs?
Web Functions run a Node.js web server that Lambda scales for you, billed by active CPU. MicroVMs are full virtual machines built from a Dockerfile, started from snapshots, suspended when idle, and billed per second. MicroVMs suit running untrusted or machine-written code in isolated sessions.
How do I delete a Lambda Web Function?
Run aws lambda-web delete-web-function --function-name my-web-app. It removes the function, endpoints and revisions, but not the IAM role, the code packages in the deployment S3 bucket, or the CloudWatch log groups. Delete those separately if nothing else uses them.
For readers preparing for an AWS certification
We have been covering AWS certifications alongside our fixes, so here is a short extra for readers studying for one. If you only came for the fix, you already have it; feel free to skip this part.
Exams that test this topic: Developer – Associate (DVA-C03 from December 1, 2026) • Solutions Architect – Associate (SAA-C03) • CloudOps Engineer – Associate (SOA-C03).
Here are the exam-style questions this topic tends to raise, with the reasoning you need to answer them:
1. A function needs its own HTTPS endpoint without API Gateway. What do you use?
A Lambda function URL.
2. Dependencies are bigger than the 250 MB unzipped limit. What is the way out?
Package the function as a container image, up to 10 GB.
3. Latency must stay steady with no cold starts during business hours. What do you use?
Provisioned concurrency, optionally scheduled with Application Auto Scaling.
4. A new version must get 10% of traffic before full rollout. How?
A weighted alias, or CodeDeploy with a canary deployment.
5. Several functions need the same helper code. What do you use?
A Lambda layer.
6. Java functions start slowly. Which feature cuts the startup time?
Lambda SnapStart.
Exam codes and dates change often. Our AWS certification changes page keeps track of what is current.
Ethan moved the shop's chat helper first, the one that waits three seconds on the AI for every answer. He switched its endpoint to IamAuth behind CloudFront, so the old public URL stopped answering strangers, and found one route that resized photos synchronously and moved it to a worker thread. The booking page stayed on classic Lambda for now, because the numbers came out even. When the next bill arrived, the chat helper's line was a third of what it used to be. Jake didn't follow the CPU math, but he understood the number at the bottom, and he asked whether the repair-status page could move next.
📌 If you keep one line from this page
Listen on AWS_LAMBDA_HTTP_ENDPOINT, lock the default public URL before you share it, and move the apps that wait, not the apps that think.
Web Functions beat classic Lambda when CPU is under about a quarter of each request's time; one blocking request slows every request in its environment.
Revision note. Written October 15, 2026, three days after AWS launched Lambda Web Functions. "One rotten apple spoils the barrel," the old saying goes; on a Web Function, one blocking request slows every request beside it.