You can now deploy Express to Lambda Web Functions without rewriting a single route into a handler, and this tutorial walks you through it from an empty terminal to a canary release with a rollback button. No adapters, no API Gateway glue, no “event” object pretending to be a request. Your Express app listens on a port, and Lambda does the rest.
We will build a small Express app with a health check and a streaming route, run it locally, ship it with one CLI command, put guardrails on its scaling, and then release a second version to 10 percent of traffic before promoting it. Every command below is something you can paste. Where an ID is generated for you, the placeholder is obvious and we say how to find the real one.
Prerequisites
Lambda Web Functions launched on October 12, 2026, so a few things need to line up before the first command works. Check these now and save yourself a confusing error later.
- An AWS account and credentials configured for the CLI. Pick one of the launch Regions: us-east-1, us-east-2, us-west-2, ap-northeast-1 (Tokyo), or eu-west-1 (Ireland).
- A recent AWS CLI that includes the
lambda-webcommand group. Step 1 verifies this, so do not guess. - Node.js 24 and npm on your machine. Node.js 24 on Arm64 is the only runtime at launch, and the function runs as
nodejs24.x. Matching it locally avoids “works on my laptop” theater. - Permissions for the deploy command. At minimum it needs
sts:GetCallerIdentity,s3:ListBucket, ands3:PutObject,s3:GetObjectands3:GetObjectVersion, plusiam:PassRole. If you let it create the defaults, which we will, it also needsiam:GetRole,iam:CreateRole,iam:PutRolePolicy,s3:CreateBucket,s3:PutBucketPolicyands3:PutBucketVersioning. Use a sandbox account if you can. - A terminal with
curl. That is the whole test harness.
How to deploy Express to Lambda Web Functions, start to finish
A quick mental model so the steps make sense. A Web Function is a Lambda form factor that runs a standard HTTP server on a Firecracker microVM, takes many requests at once, and bills on active CPU time. The pieces you will touch are the function itself, revisions (immutable snapshots of your code plus its configuration), and endpoints (an HTTPS URL that routes to one or two revisions). If you want the deep version, the Lambda Web Functions documentation covers the model, and the launch post on the AWS News Blog is the friendly overview.
The thing worth internalizing is where each setting lives. Package, architecture, request timeout, environment variables and per-environment concurrency belong to a revision, so changing them means a new revision. Auth type, auto-deployment mode, revision weights, scaling and throttling belong to the endpoint, so changing them is instant and creates nothing new. That split is what makes the canary in Steps 8 through 10 so cheap.
Step 1: Verify the CLI has Web Functions
Start with the boring check. If your CLI predates the launch, nothing else in this tutorial will work and the error will not be helpful. Ask the CLI for the help page of the new command group.
aws --version
aws lambda-web help
You should see a list of subcommands including deploy, get-web-function-endpoint, update-web-function-endpoint and delete-web-function. If the CLI complains that lambda-web is not a valid command, update the AWS CLI and try again. Also confirm which identity and Region you are about to spend money in, because the deploy command will create real resources there.
aws sts get-caller-identity
aws configure get region
Step 2: Deploy the hello-world and curl it
Before writing any code, prove the platform works end to end with the scaffold the CLI ships. One command generates a sample project in a folder named after your function and deploys it. Behind the scenes it creates a versioned S3 bucket for your packages, an execution role with CloudWatch Logs access, the function, the first revision, and a HomeRegion endpoint called dev that auto-routes to the latest revision.
aws lambda-web deploy --name my-web-app --hello-world
The command may stop and ask you to confirm before creating those defaults. Press Enter. In a CI pipeline you would pass --create to skip that prompt, and we will use it in Step 6 for exactly that reason. Once it finishes, ask for the endpoint details and pull out the domainName field.
aws lambda-web get-web-function-endpoint \
--function-name my-web-app \
--endpoint-name dev
The response includes the endpoint state and its domainName. Wait for the state to be ready, then curl the domain over HTTPS. Regional endpoints look like https://<domain-id>.<shard-id>.lambda-web.us-east-1.on.aws/, so paste whatever your output gave you.
curl -i https://<your-domain-name>/
The hello-world endpoint is unauthenticated unless you pass --auth-type IamAuth, so treat it as a throwaway. A 200 and some hello-world output means the plumbing is good: role, bucket, revision, endpoint. The first request hits a cold start since a fresh function has nothing running, so give it a moment. Everything from here on is your code. Leave the scaffold alone for now; we deploy our own folder under the same function name later.
Step 3: Create the Express project
Make a separate folder for the real app, so you are not fighting the scaffold. The contract with Lambda is tiny: a Node.js 24 project with an index.js at the package root, which is the default entry file. If you would rather name it something else, set the AWS_LAMBDA_NODEJS_ENTRYPOINT environment variable on the revision. We will keep the default because defaults are free.
mkdir express-app
cd express-app
Create a package.json. Nothing exotic here, one dependency and an engines hint. Swap Express for Hono or Fastify if that is your flavor; the platform does not care, it just wants an HTTP server.
{
"name": "express-app",
"version": "1.0.0",
"main": "index.js",
"engines": { "node": ">=24" },
"scripts": { "start": "node index.js" },
"dependencies": {
"express": "^5.1.0"
}
}
Now the packaging recommendation, because it bites people. The deploy command bundles the folder you point it at. It does not reach out and run your install for you. So install production dependencies before you deploy, and leave out dev dependencies so the package stays lean and cold starts stay short. I recommend running this every time, right before the deploy command, as a habit.
npm install --omit=dev
Step 4: Write index.js with a health check, a stream and a SIGTERM handler
This is the only part that is specific to Web Functions, and it is about six lines. Lambda hands your process an AWS_LAMBDA_HTTP_ENDPOINT environment variable in host:port form. Your server must listen on that host and port within 10 seconds of starting. Do not hardcode 8080 out of habit; that is the Lambda-adapter reflex talking, and you can retire it. If you are migrating from the older AWS Lambda Web Adapter approach, this is the part you get to delete code for.
The app has three routes. The root returns JSON including a version string, which makes the canary visible later. /health is a plain 200 for you and for any monitor. /stream writes five chunks half a second apart, and because responses stream without extra configuration, you will see them arrive one at a time. That is the same mechanism you would use for LLM token streaming.
The SIGTERM handler matters more than it looks. When Lambda drains an environment it sends SIGTERM and you get up to one second to clean up. There is no guaranteed environment lifetime, so treat shutdown as a normal event, not an edge case. Flush buffered logs, close what you can, and exit. The timer below is a safety net, since an open streaming connection would otherwise keep server.close waiting.
const express = require('express');
// Lambda tells us where to listen, in "host:port" form.
const [host, port] = process.env.AWS_LAMBDA_HTTP_ENDPOINT.split(':');
const VERSION = 'v1';
const app = express();
app.get('/', (req, res) => {
res.json({ message: 'Hello from Express on Lambda Web Functions', version: VERSION });
});
app.get('/health', (req, res) => {
res.status(200).send('ok');
});
app.get('/stream', async (req, res) => {
res.setHeader('Content-Type', 'text/plain; charset=utf-8');
for (let i = 1; i <= 5; i++) {
res.write(`chunk ${i} from ${VERSION}\n`);
await new Promise((resolve) => setTimeout(resolve, 500));
}
res.end('done\n');
});
const server = app.listen(Number(port), host, () => {
console.log(`listening on ${host}:${port} (${VERSION})`);
});
process.on('SIGTERM', () => {
// Finish short cleanup, such as flushing buffered logs, within 1 second.
server.close(() => process.exit(0));
setTimeout(() => process.exit(0), 800).unref();
});
There is a trap hiding in this setup, so read this twice. Lambda does not verify that your server is listening when it creates a revision. A revision can show as Active while every request to it fails, because your code crashed on startup or bound to the wrong port. That is why we test locally first and hit a health route right after every deploy.
Step 5: Run it locally with the same variable Lambda uses
Local testing is wonderfully dull, which is the point. You set the same environment variable Lambda sets, start the process, and use curl. No emulator, no container, no SAM template. In one terminal, start the server.
AWS_LAMBDA_HTTP_ENDPOINT=127.0.0.1:3000 node index.js
In a second terminal, hit all three routes. The -N flag turns off curl’s output buffering so you can watch the stream arrive in pieces rather than all at once.
curl -i http://127.0.0.1:3000/
curl -i http://127.0.0.1:3000/health
curl -N http://127.0.0.1:3000/stream
You should see the JSON with "version":"v1", an ok, and five chunks about half a second apart. To exercise the real shutdown path, run kill -TERM against the process ID from another shell and confirm it exits quickly.

Step 6: Deploy your code, then get the endpoint and curl it
Time to ship. From the folder that contains express-app, run the deploy command with --code pointing at your project. Because my-web-app already exists from Step 2, this creates a new revision of it, and the dev endpoint, still on auto-deployment, routes to it on its own. Run the install first so the bundle includes your production dependencies.
cd express-app && npm install --omit=dev && cd ..
aws lambda-web deploy --name my-web-app --code ./express-app
In a script or CI job, there is no human to press Enter, so add --create. If your organization already has a bucket and an execution role you must use, pass them explicitly instead of letting the CLI create defaults. You can also deploy to several Regions in one go with --regions.
# CI-friendly: no prompt
aws lambda-web deploy --name my-web-app --code ./express-app --create
# Bring your own bucket and role
aws lambda-web deploy --name my-web-app --code ./express-app \
--bucket-name amzn-s3-demo-bucket \
--execution-role-arn arn:aws:iam::111122223333:role/my-web-app-execution-role
# Multiple Regions in one command
aws lambda-web deploy --name my-web-app --code ./express-app \
--regions us-east-1,eu-west-1,ap-northeast-1
Run only the first variant for this tutorial. Then fetch the endpoint and test the three routes over HTTPS; the /health call is the real signal that your server came up.
aws lambda-web get-web-function-endpoint \
--function-name my-web-app \
--endpoint-name dev
curl -i https://<your-domain-name>/health
curl -s https://<your-domain-name>/
curl -N https://<your-domain-name>/stream
aws lambda-web list-web-function-revisions --function-name my-web-app
Step 7: Put guardrails on scaling and throttling
Scale-to-zero is lovely until a bug, a crawler or a very enthusiastic customer turns it into scale-to-your-credit-card. Web Functions give you two endpoint-level brakes, and because they are endpoint settings, applying them creates no new revision and takes effect without a redeploy.
maxEnvironments caps how many execution environments the endpoint can run, anywhere from 2 to 10,000 per Region. rateLimit caps requests per second per Region, from 0 to 10,000. Up to 1,000 it must be a multiple of 100, and above 1,000 a multiple of 1,000. Setting it to 0 blocks all traffic, which is a handy kill switch, and callers will see a 503 with Platform.EndpointDisabled. Do the math on your worst case too: peak in-flight requests are roughly maxEnvironments times maxConcurrencyPerEnvironment, and that second value defaults to 64 (the allowed range is 1 to 128).
aws lambda-web update-web-function-endpoint \
--function-name my-web-app \
--endpoint-name dev \
--scaling-config '{"maxEnvironments": 50}'
aws lambda-web wait web-function-endpoint-updated \
--function-name my-web-app \
--endpoint-name dev
aws lambda-web update-web-function-endpoint \
--function-name my-web-app \
--endpoint-name dev \
--throttle-config '{"rateLimit": 500}'
aws lambda-web wait web-function-endpoint-updated \
--function-name my-web-app \
--endpoint-name dev
Know how the limits fail, because clients need to handle both. A 429 means you hit the rate limit, the environment cap, or the account vCPU quota. A 503 without a throttle header is load shedding, and the right client behavior is retry with exponential backoff and jitter. Also note that when Lambda simply lacks warm capacity, requests wait through a cold start rather than being rejected instantly, so a slow first request is not the same thing as a throttle. The scaling documentation lists the quotas, and each environment uses 2 vCPUs against an account-level limit, so check that before setting a big number.
Step 8: Turn off auto-deployment and ship v2 as a new revision
Right now the dev endpoint is on LatestRevision, which means every deploy goes live instantly. That is perfect for development and exactly wrong for a release you want to ramp. Switch the endpoint to Disabled first. From then on, new revisions get created but no traffic moves until you say so. Revision weights, which we use in the next step, only work in this mode.
aws lambda-web update-web-function-endpoint \
--function-name my-web-app \
--endpoint-name dev \
--auto-deployment-mode Disabled
aws lambda-web wait web-function-endpoint-updated \
--function-name my-web-app \
--endpoint-name dev
Before you change anything, write down the ID of the revision serving traffic now. That is your known-good version, and the rollback in Step 10 points straight back at it. Then make a visible change to the app. Edit express-app/index.js and change const VERSION = 'v1'; to const VERSION = 'v2';, using your editor or the one-liner below. On macOS the in-place flag for sed needs an empty string argument, so just use your editor if that trips you.
aws lambda-web list-web-function-revisions --function-name my-web-app
sed -i "s/const VERSION = 'v1'/const VERSION = 'v2'/" express-app/index.js
grep "const VERSION" express-app/index.js
Now deploy again. Same command as before, same install habit. The difference is the endpoint no longer follows the newest revision, so the deploy creates revision two and leaves your users on revision one. Revisions are immutable snapshots, so v1 is exactly as you left it.
cd express-app && npm install --omit=dev && cd ..
aws lambda-web deploy --name my-web-app --code ./express-app
aws lambda-web list-web-function-revisions --function-name my-web-app
The listing now shows two revisions. Copy both IDs. In the commands that follow, rev-abc12 is the old one (v1) and rev-def45 is the new one (v2); substitute your real values. Then wait for the new revision to become Active, because weights can only point at Active revisions. Remember Active does not mean your server started, so you cannot lean on that status alone.
aws lambda-web wait web-function-revision-active \
--function-name my-web-app \
--revision-id rev-def45
Step 9: Canary 90/10, watch, then move to 100
This is the payoff. One endpoint can route to up to two revisions, with integer weights from 1 to 100 that must add up to exactly 100. Send 90 to the old revision and 10 to the new one. Only the endpoint changes, so no rebuild and no new URL.
aws lambda-web update-web-function-endpoint \
--function-name my-web-app \
--endpoint-name dev \
--revision-weights '[{"revisionId": "rev-abc12", "weight": 90}, {"revisionId": "rev-def45", "weight": 10}]'
aws lambda-web wait web-function-endpoint-updated \
--function-name my-web-app \
--endpoint-name dev
Now generate some traffic and watch the split. Roughly one in ten responses should report v2. A small loop makes that obvious, and the /health check keeps you honest about whether the new revision is actually answering.
for i in $(seq 1 40); do
curl -s https://<your-domain-name>/ ; echo
done | sort | uniq -c
Now wait, and use the wait for something. In a real release you hold at 10 percent for as long as it takes to be confident, and that is a judgment about your traffic, not a magic number. While you wait, watch your application logs in CloudWatch and the endpoint metrics: EndpointRequestThrottles, EndpointActiveEnvironments and EnvironmentConcurrentRequests are the useful ones here, and the monitoring documentation lists the rest. If you want a middle rung, go 50/50 first with the same command and different weights, pause again, then continue.
# Hold at 10 percent while you watch logs and metrics
sleep 600
# Optional middle rung
aws lambda-web update-web-function-endpoint \
--function-name my-web-app \
--endpoint-name dev \
--revision-weights '[{"revisionId": "rev-abc12", "weight": 50}, {"revisionId": "rev-def45", "weight": 50}]'
aws lambda-web wait web-function-endpoint-updated \
--function-name my-web-app \
--endpoint-name dev
sleep 600
When the numbers look clean, promote v2 by giving it all the weight. A single revision at 100 is a valid configuration, and the old revision stays around untouched in case you change your mind.
aws lambda-web update-web-function-endpoint \
--function-name my-web-app \
--endpoint-name dev \
--revision-weights '[{"revisionId": "rev-def45", "weight": 100}]'
aws lambda-web wait web-function-endpoint-updated \
--function-name my-web-app \
--endpoint-name dev
curl -s https://<your-domain-name>/
Step 10: Know the rollback command before you need it
Rollback is not a redeploy. It is the same endpoint update pointed back at the previous revision with full weight, and since v1 is an immutable snapshot, you get the exact bits you ran before. It takes effect as an endpoint change, with nothing to rebuild or upload. Run it during the canary at 10 percent, or after promotion if something slow and nasty shows up later.
aws lambda-web update-web-function-endpoint \
--function-name my-web-app \
--endpoint-name dev \
--auto-deployment-mode Disabled \
--revision-weights '[{"revisionId": "rev-abc12", "weight": 100}]'
aws lambda-web wait web-function-endpoint-updated \
--function-name my-web-app \
--endpoint-name dev
curl -s https://<your-domain-name>/
What you built (and how to clean it up)
You went from nothing to an Express app running on a Lambda Web Function with a health check, a streaming route and a graceful SIGTERM handler. You tested it locally using the same environment variable Lambda uses, deployed it with one command, capped its scaling and request rate, then released a second version to 10 percent of traffic and promoted it, with a rollback you have now seen work. All of that without adapters or a rewrite, and without a deployment tool beyond the AWS CLI.
Where to go from here depends on what you are building. The endpoint is open to the internet in this tutorial, so for anything real, choose IAM auth or put CloudFront in front. The CloudFront origin access control guide shows how to restrict an origin so only your distribution can invoke it, and CloudFront is also where custom domains and WAF come in. Remember that Web Functions are for HTTP workloads; queues and file-processing triggers still belong to Event Functions. Billing covers requests, active CPU time, memory for the life of each environment including idle gaps, and data transfer, so read the Lambda pricing page before you point real traffic at it.
Now clean up so the demo does not linger. One command deletes the function, and the CLI is blunt about it.
aws lambda-web delete-web-function --function-name my-web-app
What it does not remove is the scaffolding the deploy command created for you. Delete these yourself if you do not plan to reuse them:
- The execution role, named
awscli-lambdaweb-my-web-app. - The uploaded packages under the
lambda-web/my-web-app/prefix in the bucket namedawscli-lambdaweb-<account>-<region>-an. The bucket is versioned, so empty old versions as well. - The CloudWatch log groups your function wrote to.
- Your local
my-web-appandexpress-appfolders, which are just files.
ENKOMPASS IS HERE TO HELP
At Enkompass (enkompass.net), this is our home turf. We design and run serverless and container workloads on AWS every day, including the guardrails, canary releases and rollbacks that keep a new platform like Lambda Web Functions from becoming a production surprise. If you’d rather have seasoned cloud people handle it instead of learning the hard way at 2 AM, let’s talk. Reach out through our contact page or call us directly at +1-828-436-5667 — we’ll help you turn this from a someday-project into a done-deal.
