AWS launched Lambda Web Functions in October 2026, and the headline claim is easy to test. Take a plain Express server, run aws lambda-web deploy, and a live HTTPS URL comes back. AWS says that takes under a minute, and InfoQ’s launch coverage frames the release as a Node.js-first move that drops the event-handler rewrite most teams hated. This tutorial takes you from an empty folder to a canary rollout and a multi-Region endpoint in 13 steps.
You will build lambda-notes, a small Express 5.3.0 service with a notes API, a health check, a server-sent events stream, and a CPU burner for load tests. Plan on about 50 minutes, and most of that goes to reading logs rather than waiting on deploys. If you are still choosing a host, our breakdown of Lambda Web Functions against Cloudflare Workers and Vercel covers the trade-offs. This guide stays hands-on.
Everything below follows AWS’s own launch post, developer guide and compute blog as of October 27, 2026. Where AWS has not published a flag or a subcommand, the article says so and sends you to aws lambda-web help instead of guessing. The service is two weeks old. A guessed flag is the quickest way to burn an afternoon.
Don't miss new tech stories on Google
Add Tech Insider once in the Google app and our stories appear in your news suggestions.
What Lambda Web Functions Change in 2026
Until October 12, every Lambda function was an event handler. Your code exported a function, Lambda passed it an event, and you returned a result. Running Express meant bolting on an adapter that translated API Gateway events into HTTP requests. Lambda Web Functions remove that layer. AWS now describes three form factors on the same Firecracker isolation: Event Functions (the functions you already run, renamed with no code changes or redeploys), Web Functions for HTTP servers, and MicroVMs for stateful sandboxes. If you need a refresher on the older model, our walkthrough on how to set up AWS Lambda covers Event Functions end to end.
The AWS Lambda developer guide puts the framework promise plainly: “You can use familiar Node.js web frameworks such as Express, Hono, or React Router with no Lambda-specific adapters or wrappers.” The same page says you deploy a .zip file, Lambda creates an HTTPS endpoint, and the service scales your app with traffic, including down to zero when idle. That last point matters for cost, and the pricing section below shows where it helps and where it does not.
Three mechanics separate Web Functions from the Lambda you know. First, one execution environment serves many requests at once, up to 128 concurrent requests per environment, instead of one request per environment. Second, billing meters active CPU time, so a request that waits on a database consumes little CPU while it waits. Memory, however, is metered for as long as the environment exists, including idle gaps. Third, you get a built-in HTTPS endpoint, so API Gateway is no longer mandatory for the demonstrated workflow. Teams comparing serverless platforms can see how this shifts the field in our AWS Lambda vs Azure Functions vs GCP comparison.
There are limits worth knowing before you invest an hour. Web Functions run Node.js only. If your service is Java, Python or .NET, AWS points you to Event Functions or MicroVMs instead. At launch the runtime is Node.js 24 (nodejs24.x) on Arm64, and the service is available in five Regions: US East (N. Virginia), US East (Ohio), US West (Oregon), Asia Pacific (Tokyo) and Europe (Ireland). IT Brief’s launch report lists the same five. AWS also lists VPC connectivity, so a Web Function can reach private databases.
Choosing between the three forms comes down to the shape of the work. A rule of thumb that holds up in practice:
- Web Functions for REST and GraphQL APIs, server-side rendered pages, streaming endpoints and microservices that answer HTTP traffic directly.
- Event Functions for queue consumers, file-upload handlers, scheduled jobs and anything another AWS service triggers.
- MicroVMs for running untrusted or user-generated code in isolated, stateful sandboxes with a lifecycle you manage.
- Neither when you need a runtime other than Node.js 24, or a single job that runs for many minutes with no HTTP front end.
Mixed estates are normal. AWS itself describes one application that uses Event Functions for asynchronous back-end processing, Web Functions for the user-facing API and rendering layer, and MicroVMs for interactive code execution, all in one account.
How Web Functions Work: Functions, Revisions and Endpoints
A Web Function is three resources stacked together. The function is the named top-level resource that holds configuration. A revision is an immutable snapshot of your code and configuration, and Lambda creates a new one every time you deploy code or change revision settings. An endpoint is the network entry point, with its own HTTPS domain, an authentication type, and rules that route traffic to one or two revisions. Rolling back is therefore a routing change, not a rebuild. The AWS Builder Center post Bring Your HTTP Server, Leave the Handler Behind explains the same model from the developer side.
When you run the deploy command for the first time, the CLI does more than upload a zip. According to the AWS compute blog, it bundles your code, creates the function and an endpoint named dev, and returns a live HTTPS URL. It also creates an execution role and a versioned S3 bucket if one does not exist, and every CLI-deployed Web Function in that Region shares the bucket. Remember this when you clean up, because the shared bucket and role outlive any single function.
Execution environments are long-lived processes. Your Express server starts, listens on a port, and keeps serving until Lambda scales the environment away. That is the opposite of an Event Function, where Lambda freezes the environment between invocations and background work stops. A Web Function can run background tasks outside the request path, such as emitting telemetry. It cannot count on staying alive forever, because idle environments are removed. Lambda also routes every request independently with no session affinity, which has a direct consequence for the in-memory notes store in this tutorial. You will see that consequence in Step 12. For readers who want the sandbox form factor too, our AWS Lambda MicroVMs setup guide covers the third option.
Prerequisites and Versions
You need an AWS account where you may create Lambda resources, an S3 bucket and an IAM role. A sandbox account with broad permissions is the least painful choice while you learn. The table lists what to install. Versions come from AWS’s launch material and CLI release notes, and the CLI line matters most because older builds do not know the lambda-web namespace. If your account is brand new, set up billing alerts first (our cloud computing coverage has guides for that).
| Item | Version or value | Why it matters |
|---|---|---|
| AWS CLI v2 | 2.38.6 or newer | The lambda-web command is documented in the AWS CLI 2.38.6 command reference |
| Node.js (local) | 24.x | Matches the nodejs24.x runtime, so local behavior matches production |
| Express | 5.3.0 | The version used in the published Tokyo-region hands-on test |
| AWS Region | us-east-1, us-east-2, us-west-2, ap-northeast-1 or eu-west-1 | The five launch Regions, no others at launch |
| Runtime / architecture | nodejs24.x on Arm64 | Native npm modules must have Arm64 builds |
| Permissions | Lambda, S3, IAM role creation | The CLI creates a role and a versioned bucket on first deploy |
| curl | Not listed | The –parallel flag in the load test needs it |
Permissions deserve a minute of thought. The first deploy creates an IAM execution role, a versioned S3 bucket and the function itself, so the identity running the CLI needs rights for all three. In a shared account, ask your platform team for a scoped deployment role instead of borrowing administrator credentials. Keep that role separate from the execution role your running code uses, and give the execution role only what the app touches, such as a single DynamoDB table once you add persistence.
Check your local Node.js version before anything else. The tutorial code uses ES modules, node:test and the built-in fetch, all of which Node.js 24 supports. Older Node.js lines can run the code locally for a quick look, but you lose the guarantee that local and cloud behavior match.
Steps 1 to 3: Prepare the CLI and Run a Hello-World Smoke Test
Step 1: Confirm your AWS CLI knows lambda-web
Start by proving the command exists. A stale CLI is the number one reason first-time deploys fail, and the error message does not mention versions.
aws --version
aws lambda-web help | head -n 20
The first line should report the AWS CLI version, which should be 2.38.6 or newer. The second prints the subcommand list, which doubles as your reference for anything this article does not spell out. If the CLI answers that lambda-web is not a valid choice, upgrade with your package manager or the official installer and run both commands again.
aws-cli/2.38.6 ...
Step 2: Set credentials and pick a launch Region
Use SSO or an access key, whichever your organization allows. Then pin one of the five supported Regions so the deploy does not wander to a default Region that lacks the service.
aws configure sso # or: aws configure
export AWS_DEFAULT_REGION=us-east-1
aws sts get-caller-identity
The sts get-caller-identity call returns your account ID and ARN. If it fails, fix credentials now, because every later error will blame the wrong thing. You can also pass --region on the deploy command, which is what the Tokyo hands-on did with ap-northeast-1.
Step 3: Deploy the built-in hello-world app
Before touching your own code, let AWS generate a sample. This isolates account and permission problems from application problems. The launch post gives the exact command.
aws lambda-web deploy --name hello-world --hello-world
The first run asks permission to create the supporting resources (the Tokyo write-up shows a prompt that says to press Enter, and passes --create to skip it in scripts). Output looks like this, abridged, with the ID portion of the URL masked:
Creating starter application in ./hello-world ...
Creating deployment package ...
Preparing Amazon S3 bucket ...
Preparing AWS IAM execution role ...
Uploading deployment package ...
Creating Lambda Web Function ...
https://xxxxxxxxxxxxxxxx.xxx.lambda-web.us-east-1.on.aws
Developers at Classmethod timed the same flow in Tokyo and saw a public URL after about 24 to 26 seconds, with the full write-up in their DevelopersIO hands-on. Hit the URL with curl -sS -i and expect a 200 response with a small JSON body. The response carries a lambda-web-request-id header and a lambda-web-logstream header, both useful in Step 11. Once the sample answers, you know the account, Region and CLI are healthy.
Steps 4 to 6: Build the lambda-notes Express Service
Step 4: Scaffold the project
Create a fresh folder, switch it to ES modules, and pin Express 5.3.0. Pinning matters here because the deploy command packages exactly what sits in the folder.
mkdir lambda-notes && cd lambda-notes
npm init -y
npm pkg set type=module
npm pkg set scripts.start="node index.js"
npm pkg set scripts.test="node --test"
npm install [email protected]
Your package.json should now list "express": "5.3.0" under dependencies, plus "type": "module". Express 5 changed a few habits. Rejected promises in async handlers now flow to the error middleware automatically, req.body is undefined when no body parser matched, and wildcard routes need a name such as /*splat. Our Hono vs Express vs Fastify comparison shows how much these frameworks differ when you pick one for a new service. Each of them works on Web Functions without adapters, according to AWS.
Step 5: Write the application factory
Keep the Express app in its own module and export a factory. That split lets the test suite start the app on a random port, and it keeps index.js tiny. The app has six behaviors: an info route, a health check, a notes CRUD API held in memory, an SSE stream, a CPU burner, and JSON error handling.
// app.js
import express from 'express';
import { randomUUID } from 'node:crypto';
export function createApp() {
const app = express();
const notes = new Map();
const startedAt = Date.now();
let openStreams = 0;
app.disable('x-powered-by');
app.use(express.json({ limit: '16kb' }));
app.get('/', (req, res) => {
res.json({
service: 'lambda-notes',
node: process.version,
region: process.env.AWS_REGION ?? 'local',
endpoint: process.env.AWS_LAMBDA_HTTP_ENDPOINT ?? 'not set',
pid: process.pid,
uptimeMs: Date.now() - startedAt,
openStreams,
requestId: req.get('lambda-web-request-id') ?? null,
});
});
app.get('/healthz', (req, res) => {
res.status(200).type('text/plain').send('ok');
});
app.get('/api/notes', (req, res) => {
res.json({ count: notes.size, notes: [...notes.values()] });
});
app.post('/api/notes', (req, res) => {
const text = req.body?.text;
if (typeof text !== 'string' || text.trim() === '') {
return res.status(400).json({ error: 'text must be a non-empty string' });
}
const note = { id: randomUUID(), text: text.trim(), createdAt: new Date().toISOString() };
notes.set(note.id, note);
res.status(201).json(note);
});
app.get('/api/notes/:id', (req, res) => {
const note = notes.get(req.params.id);
if (!note) return res.status(404).json({ error: 'not found' });
res.json(note);
});
app.delete('/api/notes/:id', (req, res) => {
res.status(notes.delete(req.params.id) ? 204 : 404).end();
});
app.get('/events', (req, res) => {
res.set({ 'Content-Type': 'text/event-stream', 'Cache-Control': 'no-cache' });
res.flushHeaders();
openStreams += 1;
const timer = setInterval(() => {
res.write(`data: ${JSON.stringify({ time: Date.now(), openStreams })}\n\n`);
}, 1000);
res.on('close', () => {
clearInterval(timer);
openStreams -= 1;
});
});
app.get('/cpu', (req, res) => {
const ms = Math.min(Number(req.query.ms) || 500, 3000);
const end = Date.now() + ms;
let spins = 0;
while (Date.now() < end) spins += 1;
res.json({ burnedMs: ms, spins });
});
app.use((req, res) => {
res.status(404).json({ error: 'route not found', path: req.path });
});
app.use((err, req, res, next) => {
console.error(JSON.stringify({ level: 'error', message: err.message, path: req.path }));
res.status(err.status ?? 500).json({ error: 'internal error' });
});
return app;
}
Two choices deserve a note. The notes live in a Map inside the factory, which is fine for a demo and wrong for production, as Step 12 proves. The /cpu route caps its busy loop at 3,000 ms so nobody can park a core forever on your bill.
Step 6: Write the entry file that reads the Lambda address
This file is where Lambda Web Functions differ from every Express tutorial you have read. The runtime tells your server where to listen through an environment variable. The AWS launch post says: “The only difference from running locally is the AWS_LAMBDA_HTTP_ENDPOINT environment variable, which Lambda sets to the host and port your server must listen on.”
// index.js
import { createApp } from './app.js';
const endpoint = process.env.AWS_LAMBDA_HTTP_ENDPOINT || '0.0.0.0:3000';
const [host, port] = endpoint.split(':');
createApp().listen(Number(port), host, () => {
console.log(JSON.stringify({ level: 'info', message: 'listening', host, port: Number(port) }));
});
The fallback to 0.0.0.0:3000 mirrors the default in AWS’s compute-blog sample, so node index.js works with zero setup. By default the runtime loads index.js from the package root. If you want a different entry file, set AWS_LAMBDA_NODEJS_ENTRYPOINT to its path. Log JSON lines rather than free text, since CloudWatch Logs Insights can then filter on fields later.
Steps 7 and 8: Run and Test the Service Locally
Step 7: Start the server and exercise every route
Set the same variable Lambda would set, then run the server. Use a port that is free on your machine. Another process on port 3000 will quietly answer your curl calls and send you chasing phantom bugs, so check what is listening if a response looks wrong.
AWS_LAMBDA_HTTP_ENDPOINT=127.0.0.1:3000 node index.js
In a second terminal, create a note, list notes, burn some CPU and watch the stream:
curl -s localhost:3000/
curl -s -X POST localhost:3000/api/notes \
-H 'content-type: application/json' -d '{"text":"first note"}'
curl -s localhost:3000/api/notes
curl -s "localhost:3000/cpu?ms=200"
timeout 3 curl -sN localhost:3000/events
Expected output, with IDs and timestamps that will differ on your machine:
{"service":"lambda-notes","node":"v24.x","region":"local","endpoint":"127.0.0.1:3000","pid":4182,"uptimeMs":873,"openStreams":0,"requestId":null}
{"id":"97fc3ed3-7355-4708-8fe7-5c2fe448b200","text":"first note","createdAt":"2026-10-27T06:50:51.749Z"}
{"count":1,"notes":[{"id":"97fc3ed3-7355-4708-8fe7-5c2fe448b200","text":"first note","createdAt":"2026-10-27T06:50:51.749Z"}]}
{"burnedMs":200,"spins":3546447}
data: {"time":1793083852983,"openStreams":1}
data: {"time":1793083853984,"openStreams":1}
The spins count varies with your CPU. The requestId is null locally because only Lambda adds the lambda-web-request-id header. A POST with no body should return 400, which confirms Express 5 hands you an undefined req.body and the optional chaining catches it.
Step 8: Add automated tests with node:test
A deploy takes seconds, but a bad deploy still costs you a rollback and a confused teammate. Four small tests protect the core behavior. They start the app on port 0, so the operating system picks a free port, and they use the built-in fetch.
// test/app.test.js
import { test, before, after } from 'node:test';
import assert from 'node:assert/strict';
import { createApp } from '../app.js';
let server;
let base;
before(async () => {
server = createApp().listen(0, '127.0.0.1');
await new Promise((resolve) => server.once('listening', resolve));
base = `http://127.0.0.1:${server.address().port}`;
});
after(() => server.close());
test('GET /healthz returns ok', async () => {
const res = await fetch(`${base}/healthz`);
assert.equal(res.status, 200);
assert.equal(await res.text(), 'ok');
});
test('POST then GET a note', async () => {
const created = await fetch(`${base}/api/notes`, {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify({ text: ' ship it ' }),
});
assert.equal(created.status, 201);
const note = await created.json();
assert.equal(note.text, 'ship it');
const fetched = await fetch(`${base}/api/notes/${note.id}`);
assert.equal(fetched.status, 200);
assert.deepEqual(await fetched.json(), note);
});
test('POST rejects an empty body', async () => {
const res = await fetch(`${base}/api/notes`, { method: 'POST' });
assert.equal(res.status, 400);
});
test('unknown routes return JSON 404', async () => {
const res = await fetch(`${base}/nope`);
assert.equal(res.status, 404);
assert.equal((await res.json()).error, 'route not found');
});
$ npm test
ok 1 - GET /healthz returns ok
ok 2 - POST then GET a note
ok 3 - POST rejects an empty body
ok 4 - unknown routes return JSON 404
# tests 4
# pass 4
# fail 0
If all four pass, the code is ready for the cloud. If one fails, fix it here. Debugging inside a serverless environment costs more time than debugging on your laptop.
Steps 9 and 10: Deploy to Lambda and Verify the Live Endpoint
Step 9: Install production dependencies and deploy
One detail trips up almost everyone. The Tokyo hands-on from Classmethod notes that deploy does not resolve dependencies, so node_modules must exist before you run it. Install first, trimmed to production packages, then deploy the folder.
npm ci --omit=dev
aws lambda-web deploy --name lambda-notes --code .
If you prefer one command, add a deploy script to package.json that chains both steps, then run npm run deploy from then on.
{
"name": "lambda-notes",
"version": "1.0.0",
"private": true,
"type": "module",
"main": "index.js",
"scripts": {
"start": "node index.js",
"test": "node --test",
"deploy": "npm ci --omit=dev && aws lambda-web deploy --name lambda-notes --code ."
},
"dependencies": {
"express": "5.3.0"
}
}
Lambda reads the zip, creates the function, publishes the first revision and attaches the dev endpoint. Because you ran the hello-world deploy in Step 3, the bucket and role exist already, so the output is shorter:
Creating deployment package ...
Uploading deployment package ...
Creating Lambda Web Function ...
https://xxxxxxxxxxxxxxxx.xxx.lambda-web.us-east-1.on.aws
Copy the URL into a shell variable so later commands stay readable: export URL=https://xxxxxxxxxxxxxxxx.xxx.lambda-web.us-east-1.on.aws. Deploy it again after any edit, and the CLI detects the existing function, uploads a new revision and points the endpoint at it. AWS reports turnaround from code change to live in seconds.
Step 10: Verify the endpoint with curl
Hit the root route first. Notice what you did not create: no API Gateway, no load balancer, no adapter package. Our guide to building an AWS Lambda API shows the older event-handler route with API Gateway if you want to compare the setup effort.
curl -sS -i $URL/
HTTP/1.1 200 OK
content-type: application/json; charset=utf-8
lambda-web-request-id: XXXXXXXXXXXXXXXXXXXXXXXXXX
{"service":"lambda-notes","node":"v24.x","region":"us-east-1","endpoint":"0.0.0.0:3000","pid":2,"uptimeMs":31,"openStreams":0,"requestId":"XXXXXXXXXXXXXXXXXXXXXXXXXX"}
The Tokyo hands-on printed an endpoint of 0.0.0.0:3000 from inside the environment, which tells you what AWS_LAMBDA_HTTP_ENDPOINT holds in the cloud. Your region field should match the Region you deployed to, and node should report a 24.x version. Now post a note, read it back and watch the stream for six seconds:
curl -s -X POST $URL/api/notes -H 'content-type: application/json' -d '{"text":"hello from the cloud"}'
curl -s $URL/api/notes
timeout 6 curl -sN -i $URL/events
The stream should arrive one event per second with content-type: text/event-stream. AWS documents response streaming as working without extra configuration, and the Classmethod test measured gaps of roughly 993 to 1,002 ms between events. If the events arrive in one lump at the end, something between you and Lambda is buffering. Check for a corporate proxy before suspecting the service.
Steps 11 and 12: Read Logs and Tune Concurrency
Step 11: Tail logs and query per-environment summaries
Log groups for Web Functions follow the pattern /aws/lambda/web/FUNCTION_NAME. The AWS compute blog uses /aws/lambda/web/lambda-web-sse-dashboard for a function named lambda-web-sse-dashboard, so yours is /aws/lambda/web/lambda-notes. Stream it live while you send traffic:
aws logs tail /aws/lambda/web/lambda-notes --follow
The runtime also writes a summary record per environment every minute, with the instance ID, request count, peak concurrency and CPU time. This Logs Insights query, adapted from the AWS compute blog, rolls those records up across the fleet for the last five minutes:
QID=$(aws logs start-query \
--log-group-name /aws/lambda/web/lambda-notes \
--start-time $(($(date +%s)-300)) --end-time $(date +%s) \
--query-string 'filter type="platform.invokeSummary" | stats count_distinct(record.instanceId) as environments, sum(record.metrics.requests) as totalRequests, max(record.metrics.concurrentRequests) as peakConcurrency, sum(record.metrics.cpuTimeMs)/1000 as totalCpuSeconds' \
--query queryId --output text) && sleep 6 && aws logs get-query-results --query-id "$QID"
Those four numbers (environments, total requests, peak concurrency and CPU seconds) tell you whether the service behaves like a Web Function should. Many requests on few environments with low CPU seconds means the model is working. If you see one environment per request, something is serializing your traffic.
Step 12: Generate load and read the scaling settings
Web Functions expose three knobs. maxConcurrencyPerEnvironment sets how many requests one environment handles at once, up to 128 with a default of 64. On the endpoint, maxEnvironments caps the number of environments and rateLimit caps requests per second. Both endpoint limits apply in each Region the endpoint serves. Requests above a limit receive HTTP 429 and are not queued. Use the endpoint limits to shield databases and third-party APIs. The exact CLI flags for changing these are not spelled out in the launch material, so list them with aws lambda-web help before you touch production.
Now push load through the streaming route, which holds connections open and exposes concurrency clearly. This command, borrowed from the AWS compute blog, sends 300 streaming requests with up to 200 open at once:
curl -sN --parallel --parallel-immediate --parallel-max 200 --max-time 90 "$URL/events?[1-300]"
In AWS’s own run of that pattern against its sample dashboard, the logs showed 5 execution environments, 484 requests handled, a peak of 59 concurrent connections on one environment and about 4.75 CPU seconds across the whole fleet. Event Functions serve one request per environment, so 200 open streams would need up to 200 environments. Your numbers will differ, but the shape should match. Run the Insights query from Step 11 again and compare.
Then prove the earlier warning about in-memory state. Create several notes through $URL/api/notes after the load test, then list them repeatedly. If Lambda has spun up more than one environment, some list calls return notes that others do not, because each environment owns its own Map. Production code needs a shared store such as DynamoDB. For jobs that outgrow a request, such as many-minute batch work, see our guide to the Lambda 90-minute timeout setup.
Step 13: Roll Out a Canary and Go Multi-Region
Step 13a: Shift 10 percent of traffic to a new revision
Edit something visible, such as the service string in the root route, and run the Step 9 deploy again. By default the CLI points the endpoint at the new revision straight away. For a gradual rollout, AWS documents one API call that splits traffic by weight. The launch post shows this exact shape, which sends 90 percent of endpoint traffic to the current revision and 10 percent to the new one:
aws lambda-web update-web-function-endpoint \
--function-name lambda-notes \
--endpoint-name dev \
--auto-deployment-mode Disabled \
--revision-weights '[{"revisionId": "<current-revision-id>", "weight": 90}, {"revisionId": "<new-revision-id>", "weight": 10}]'
Replace the two placeholders with real revision IDs. The deploy output and the revision listing subcommands in aws lambda-web help show them. An endpoint routes to at most two revisions, so a canary is a two-way split. Watch the Step 11 query and the 5xx rate for a few minutes. If the new revision misbehaves, set the weights back to 100 and 0. Because revisions are immutable, the old one is exactly as you left it.
Add a multi-Region endpoint
A single-Region endpoint serves everyone from one place. To cut latency for users on other continents, create a second endpoint that spans Regions. An endpoint’s type and Regions are fixed when you create it, so this is a new endpoint, not an edit:
aws lambda-web deploy --name lambda-notes --code . \
--endpoint-name global --endpoint-type MultiRegion \
--regions us-east-1,eu-west-1
Lambda replicates your code and revision configuration, including environment variables, to each Region. It centralizes logs in the home Region and gives you one global URL in the form https://DOMAIN.lambda-web.global.on.aws. Routing uses latency-based decisions on a best-effort basis, so a caller reaches the Region with the lowest latency without you managing DNS. In AWS’s own test from the United Kingdom, a single-Region endpoint in us-east-1 answered in 90 ms and the global endpoint routed to London in 16 ms. AWS stresses those are single measurements, not a benchmark.
Two caveats apply. Your in-memory notes are now split across Regions as well as environments, which makes the shared-store requirement non-negotiable. And a multi-Region endpoint only covers the five launch Regions. If you need an API Gateway front door for usage plans or API keys, our AWS API Gateway REST API tutorial shows what that layer adds.
What It Costs: A Worked Pricing Example
AWS lists Web Functions pricing at $0.30 per million requests, $0.0000276944 per active vCPU-second and $0.0000036667 per GB-second of memory in US East (N. Virginia). Tokyo costs more at $0.0000322421 per vCPU-second and $0.0000042688 per GB-second, per the Classmethod comparison. Event Functions on Arm cost $0.20 per million requests and $0.0000133334 per GB-second, with CPU included in the memory price. The AWS compute blog says Web Functions launch with 2 vCPUs and 2 GB of memory per environment, and I use that size below.
The three scenarios below are my own illustration, not AWS figures. They ignore free tiers, data transfer and rounding, and they assume Event Functions at 1 GB with 300 ms of wall-clock time per request (a typical wait on a database), against Web Functions at 20 ms of active CPU per request. The 30-day month holds 2,592,000 seconds.
To model your own service, write down four numbers: monthly requests, average active CPU seconds per request, average environment count over the month, and environment memory in GB. Requests multiply by $0.30 per million. CPU seconds multiply by the vCPU rate. Environment count times memory times the seconds in the month multiplies by the GB-second rate. Pull the first two inputs from the Step 11 query and the third from the environment count it returns. Then run the same traffic through the Event Function formula, where wall-clock duration times memory times the GB-second rate dominates. A two-column spreadsheet settles most migration debates in ten minutes.
| Scenario | Web Functions | Event Functions (Arm) | Winner |
|---|---|---|---|
| A. 10M requests/month, 1 environment alive 24/7 | $3.00 requests plus active CPU and memory charges | $2.00 requests + $40.00 compute = $42.00 | Web Functions |
| B. 100K requests/month, environment alive 2 hrs/day | $0.03 + $0.06 + $1.58 = $1.67 | $0.02 + $0.40 = $0.42 | Event Functions |
| C. 100M requests/month, 2 environments alive 24/7 | $30.00 requests plus active CPU and memory charges | $20.00 requests + $400.00 compute | Web Functions |
The pattern is clear. Idle memory is the Web Functions tax, so tiny, spiky workloads can cost more than classic Lambda. I/O-heavy services with steady traffic cost far less, because waiting is nearly free on the CPU line. Treat the break-even as workload-specific and measure with the Step 11 query before you migrate anything. Check the AWS pricing page for how the monthly free tier applies to Web Functions, since the page lists 1 million free requests and 400,000 GB-seconds for standard Lambda. For a different compute trade-off, our AWS Fargate vs Lambda analysis compares always-on containers with Lambda pricing.
Common Pitfalls
These eight mistakes account for most wasted hours with a new serverless runtime. Several come straight from AWS documentation, and the rest follow from the architecture.
- Hard-coding the port. Lambda tells your server where to listen through
AWS_LAMBDA_HTTP_ENDPOINT. Read it, split on the colon, and fall back to a local default only when it is missing. - Skipping node_modules. The deploy command packages the folder as it is and does not run
npm install. Install production dependencies first or the first request crashes on a missing module. - Trusting in-memory state. Lambda routes each request independently with no session affinity. Sessions, caches you cannot lose and counters belong in DynamoDB, ElastiCache or another shared store.
- Ignoring the memory meter. Active-CPU billing is generous while you wait, but memory accrues for the whole life of the environment, idle time included. Low-traffic services may cost more than on Event Functions, as scenario B showed.
- Shipping the public sample. The AWS compute blog warns that its sample endpoint is public with no authentication and tells readers to delete it when finished. Pick an authentication type deliberately before real traffic arrives.
- Assuming another runtime. At launch the only runtime is
nodejs24.xon Arm64. A native module compiled for x86 will fail, and a Python service has no home here today. - Carrying over Express 4 habits. Wildcard routes need names in Express 5, and
req.bodycan be undefined. Run the tests from Step 8 before every deploy. - Forgetting the shared resources. The CLI creates one execution role and one versioned S3 bucket per Region, shared by all CLI-deployed Web Functions. Deleting one function does not remove them, and deleting them breaks your other functions.
For a broader catalogue of Lambda gotchas that still apply to Event Functions, the Cloudless Labs Lambda notes on pricing and fixes are worth a read. Most of it carries over, but check any statement about timeouts or concurrency against the Web Functions documentation, because the two models differ.
Troubleshooting: 12 Problems and Fixes
Use this table when something breaks. The fixes follow AWS documentation where it exists. Where AWS has not published an exact error string, the symptom column describes behavior instead of quoting a message.
| Symptom | Likely cause | Fix |
|---|---|---|
| CLI says lambda-web is not a valid command | An older AWS CLI that does not include the lambda-web command | Upgrade to 2.38.6 or newer and rerun aws lambda-web help |
| First request fails with a missing module error | node_modules was not in the folder at deploy time | Run npm ci –omit=dev, then deploy again |
| Requests time out though the deploy succeeded | Server listens on a hard-coded port or on localhost | Split AWS_LAMBDA_HTTP_ENDPOINT into host and port and listen on both |
| Entry file not found | Your entry is not index.js at the package root | Move it, or set AWS_LAMBDA_NODEJS_ENTRYPOINT to its path |
| HTTP 429 under load | You hit the endpoint rateLimit or maxEnvironments cap | Raise the limit or add client retries, since requests over the cap are not queued |
| Deploy fails in a Region you chose | The Region is not one of the five launch Regions | Use us-east-1, us-east-2, us-west-2, ap-northeast-1 or eu-west-1 |
| Native module crashes on start | Module built for x86, runtime is Arm64 | Rebuild on Arm64 or switch to a pure JavaScript package |
| Notes appear and vanish between calls | Several environments, no session affinity | Move state to DynamoDB or another shared store |
| 403 from an endpoint using IAM auth | Request not signed with SigV4, or caller lacks permission | Sign requests and grant lambda:InvokeWebFunctionEndpoint (SigV4a for multi-Region) |
| Cannot convert single-Region endpoint to global | Endpoint type and Regions are fixed at creation | Create a new endpoint with –endpoint-type MultiRegion |
| No logs where you expect them | Multi-Region logs live in the home Region | Query /aws/lambda/web/NAME in the Region you deployed from |
| Script hangs waiting for input on first deploy | CLI asks to create the role and bucket | Press Enter interactively or pass –create in CI |
Two further checks save time. Run aws sts get-caller-identity whenever a deploy fails with a permissions-flavored message, since an expired SSO session looks like a Lambda problem. And compare local and cloud behavior by calling the root route, which prints the Node.js version, Region and endpoint the process sees.
Advanced Tips for Production Web Functions
Pick the right authentication type
Each endpoint has an authentication type you can change without redeploying code. ApplicationManaged makes the endpoint public and leaves OAuth, JWT, cookies or Amazon Cognito to your app. IamAuth makes Lambda validate each request with AWS Signature Version 4 (SigV4a for multi-Region endpoints), and only callers with the lambda:InvokeWebFunctionEndpoint permission get through. That fits service-to-service APIs. Security teams can block public endpoints organization-wide with a service control policy using the lambda:Request/WebFunctionAuthType condition key.
Put CloudFront in front
Endpoints use a Lambda-provided domain. For a custom domain, edge caching and AWS WAF, place Amazon CloudFront in front. AWS recommends pairing IamAuth with CloudFront origin access control, so CloudFront signs requests and the Lambda domain cannot be used to bypass the distribution. Your application’s own Authorization header still reaches your code. Static assets then come from cache and only dynamic routes hit the function.
Keep local and cloud behavior aligned
Because the contract is just an environment variable and a port, your local workflow stays ordinary. Run the server on Node.js 24, set AWS_LAMBDA_HTTP_ENDPOINT, and run the same npm test in CI before every deploy. Add a smoke test that calls /healthz on the deployed URL right after aws lambda-web deploy returns, and fail the pipeline unless it answers 200. That single check catches wrong ports, missing dependencies and broken entry points before users do. Pair it with the canary split from Step 13 and you have a delivery pipeline that costs a few lines of shell.
More levers worth pulling
- Use CloudFormation, SAM or CDK for repeatable stacks. AWS says Web Functions are configurable through the Lambda API, CLI, console, CloudFormation, SAM, CDK and SDKs.
- Run Next.js in standalone mode with a small
index.jsthat sets its host and port fromAWS_LAMBDA_HTTP_ENDPOINT. Fastify and Hono need no such shim. - Cap
maxEnvironmentsto the connection limit of your database. It turns a traffic spike into clean 429s instead of an exhausted connection pool. - Stream LLM tokens straight from the handler. Response streaming needs no extra setup, and waiting on a model call is cheap on the CPU meter.
- Let a coding agent scaffold the project. AWS says the Agent Toolkit for AWS includes Web Functions, and the single-command deploy suits agent workflows.
- Watch the SDK surface. According to their changelogs, AWS SDK for JavaScript v3 and Go v2 recorded the Web Functions launch on October 12 and 13, and the Java SDK documents a
LambdaWebClient, so you can automate deploys beyond the CLI.
Clean Up Your Resources
The compute blog warns that its sample endpoint is public, so delete what you create. List the available subcommands with aws lambda-web help and use the delete operation for the function and its endpoints. I am not printing a delete command from memory, because AWS has not published one in the launch material I reviewed. Then remove the log group, which keeps the name pattern from Step 11:
aws logs delete-log-group --log-group-name /aws/lambda/web/lambda-notes
aws logs delete-log-group --log-group-name /aws/lambda/web/hello-world
Leave the shared execution role and versioned S3 bucket in place if any other CLI-deployed Web Function still uses them. If you started from a clean sandbox, remove both last. Finally, set a budget alarm so a forgotten always-on environment never surprises you. Our walkthrough on AWS Cost Explorer and Budgets takes about 90 minutes and covers it.
Frequently Asked Questions
What are AWS Lambda Web Functions?
Web Functions are a Lambda form factor, launched in October 2026, that runs a standard port-listening HTTP server directly on Lambda. You skip the event-handler rewrite and the adapter. AWS names Express, Hono, Fastify and Next.js as examples, and bills requests, active CPU, memory and data transfer.
Do I still need API Gateway?
Not for the workflow shown here. Each Web Function endpoint gets its own HTTPS domain. You can still add API Gateway or CloudFront in front if you want features like usage plans, a custom domain or WAF rules.
Which languages and runtimes are supported?
Node.js only at launch, specifically Node.js 24 on Arm64. For Java, Python or .NET, AWS directs you to Event Functions, or to MicroVMs if you need a stateful sandbox.
How long does the first deploy take?
AWS says the deploy returns a live HTTPS URL in under a minute. Classmethod measured about 24 seconds for an Express app and about 26 seconds for the hello-world sample in Tokyo, with the support resources already created for the Express run.
Is it cheaper than regular Lambda?
It depends on the workload. I/O-heavy services with steady traffic can cost far less because waiting does not bill CPU. Low-traffic services can cost more because memory accrues while an environment is alive. The pricing table above shows both outcomes.
Can I run it in several Regions?
Yes. A multi-Region endpoint replicates your code to the Regions you list and routes callers by latency through one global URL. Only the five launch Regions are available, and endpoint type and Regions cannot be edited after creation.
What happens to my existing Lambda functions?
AWS renamed them Lambda Event Functions to distinguish the programming model. The change needs no code changes or redeployment, and Event Functions remain the right choice for queues, file events and other AWS service triggers.





