Lambda Web Functions runs your Node.js HTTP server directly on Lambda, with many requests per environment and billing for active CPU. That changes the math for SSR and for APIs that spend the day waiting on a database. The catch: AWS launched it in five regions, and none of them is in South America.
If you've ever put Express on Lambda, you know the ritual: Lambda Web Adapter or serverless-http translating events into req/res, and a bill that charges for every millisecond spent sitting there waiting for Postgres to answer. That's what AWS changed on October 12. And if your users are in Brazil (mine are, I'm writing from São Paulo), the launch comes with a region decision baked in.
Validity note: written in October 2026, six days after the announcement, checking limits, pricing and IaC against the official docs. Freshly launched services pick up regions and runtimes fast, and I'll update this post when that happens (the first update will be a South America region showing up). If something doesn't match what you're seeing, tell me in the comments. The living reference is the official Lambda Web Functions documentation.
What Lambda Web Functions is (and what happened to the Lambda you already use)
Lambda Web Functions is a new function format where your code is a regular HTTP server listening on a port, instead of an (event, context) handler. The AWS News Blog mentions Express, Hono, Fastify, Next.js "or any Node.js framework", with no adapter in between.
The launch also came with a rename. The Lambda you already know is now called Lambda Event Functions, with no code change and no redeploy. Lambda now has three formats, all built on Firecracker: Event Functions, Web Functions and MicroVMs. Each one serves a different programming model (I broke down the third one in my post on Lambda MicroVMs for untrusted code isolation).
The new vocabulary, anchored in what you already use:
- Web Function is the top-level resource, the umbrella.
-
Revision is an immutable snapshot of code and config (.zip package in S3, runtime, timeout, env vars, execution role, per-environment concurrency, network connector). Think of an Event Function
version: change any of those fields and a new revision is born. - Endpoint is the HTTPS front door, with its own domain and TLS. It plays the role of a weighted alias: it routes to at most two revisions, with a percentage weight (integer from 1 to 100) on each. Canary and blue/green come built in, no CodeDeploy needed.
What changes in the execution and billing model
With Event Functions, every table gets its own waiter. The waiter takes the order, walks it to the kitchen, then stands next to the table waiting for the dish. You pay that waiter for the whole time standing there, including the time spent staring at the kitchen door. That's the "1 request = 1 environment, billed by wall-clock duration times configured memory" model.
With Web Functions, one waiter covers several tables: while one dish is in the kitchen, they take the order at the next table. And you only pay for the time they're actually working. In the docs, that turns into two changes.
The first is per-environment concurrency: each execution environment serves several requests in parallel. The ceiling is maxConcurrencyPerEnvironment, set on the revision, from 1 to 128, default 64. Below that ceiling, Lambda itself adjusts the limit dynamically based on resource usage.
The second is active CPU billing. The pricing page splits it into three dimensions (us-east-1, Arm):
| Dimension | Price |
|---|---|
| Active vCPU | $0.0000276944 per vCPU-second |
| Memory | $0.0000036667 per GB-second |
| Requests | $0.30 per million |
Granularity is 1 ms, with data transfer billed separately. Time spent waiting on a query or an API call burns very little CPU, and only active CPU goes on the bill. Memory follows a different rule: the environment pays for the memory it consumes for as long as it exists, including the gaps between requests.
The rest of the execution contract, straight from the docs:
- Lambda injects
AWS_LAMBDA_HTTP_ENDPOINT(host:port), and your server has up to 10 seconds to listen on exactly that address. As soon as it's listening, Lambda marks the environment as ready and starts sending requests. - On shutdown you get a
SIGTERM, and the process has up to 1 second to clean up. - It scales to zero with no traffic. Cold starts happen when a new environment comes up; for small traffic increases, Lambda keeps some headroom of environments ahead of demand.
- The environment is long-lived and isn't frozen between requests the way an Event Function is.
Hands-on: the server and the deploy
The minimum code is an HTTP server. Here's a version with node:http, zero dependencies, following the contract in the docs:
// index.js (the runtime loads index.js from the package root by default)
const http = require('node:http');
const [host, port] = process.env.AWS_LAMBDA_HTTP_ENDPOINT.split(':');
// Global scope: process-level things only (SDK client, connection pool, config).
const startedAt = new Date().toISOString();
const server = http.createServer((req, res) => {
// Everything that belongs to the request stays in here, never in a global.
const requestId = req.headers['x-request-id'] ?? 'n/a';
if (req.url === '/health') {
res.writeHead(200, { 'content-type': 'application/json' });
return res.end(JSON.stringify({ ok: true, startedAt }));
}
res.writeHead(200, { 'content-type': 'application/json' });
res.end(JSON.stringify({ message: 'Hello from Lambda Web Functions!', requestId }));
});
server.listen(Number(port), host, () => {
console.log(`listening on ${host}:${port}`);
});
// You get up to 1 s after SIGTERM. Don't count on it to persist data.
process.on('SIGTERM', () => {
server.close(() => process.exit(0));
setTimeout(() => process.exit(0), 800).unref();
});
The x-request-id header is illustrative (the right header depends on your front door). The example is really about scope: with up to 128 requests in the same process, a global variable holding the current request's user leaks one customer's data to another.
For deploys, the fastest path is the CLI's aws lambda-web deploy. For IaC, CloudFormation already has three types: AWS::Lambda::WebFunction, AWS::Lambda::WebFunctionRevision and AWS::Lambda::WebFunctionEndpoint. The template below only uses properties that appear in the CloudFormation reference:
AWSTemplateFormatVersion: '2010-09-09'
Parameters:
CodeBucket:
Type: String
ExecutionRoleArn:
Type: String
Resources:
WebApp:
Type: AWS::Lambda::WebFunction
Properties:
FunctionName: my-api
WebAppRevision:
Type: AWS::Lambda::WebFunctionRevision
Properties:
FunctionName: !Ref WebApp # accepts name or ARN
Description: first revision
BuildConfig:
CodeConfig:
S3Object:
Bucket: !Ref CodeBucket
Key: app.zip
RuntimeConfig:
Runtime: nodejs24.x
ServiceConfig:
ExecutionRoleArn: !Ref ExecutionRoleArn
TimeoutSeconds: 30
MaxConcurrencyPerEnvironment: 32 # 1 to 128, default 64
WebAppEndpoint:
Type: AWS::Lambda::WebFunctionEndpoint
Properties:
FunctionName: !Ref WebApp
EndpointName: prod
EndpointType: HomeRegion # can't change after creation
AuthType: IamAuth # required for CloudFront with OAC
RevisionWeights:
- RevisionId: !GetAtt WebAppRevision.RevisionId
Weight: 100
ScalingConfig:
MaxEnvironments: 20 # optional, minimum 2
Outputs:
EndpointDomain:
Value: !GetAtt WebAppEndpoint.DomainName
The CLI deploy creates an ApplicationManaged endpoint (public, no Lambda-level authentication) by default, which is why the template ships with IamAuth. Also, EndpointType and regions are fixed: to change them, you create a new endpoint and move your clients to the new domain. The What's New post also mentions AWS SAM and AWS CDK; I only checked the CloudFormation reference property by property, so the template is plain CloudFormation.
When to switch from Event Functions to Web Functions
The docs list the Web Functions use cases: full-stack web apps, REST and GraphQL APIs, HTTP microservices, SSR and response streaming (LLM output, for example). Queues and file uploads, per the same docs, stay on Event Functions.
My rule of thumb is the ratio between CPU and waiting. If a request takes 200 ms end to end and 50 ms of that is CPU (the rest is database, external API, AI model), on Event Functions you pay for the 200 ms; on Web Functions, you pay for the 50 ms of CPU plus the environment's memory. LLM streaming is the extreme case: seconds of open connection, almost no CPU.
The reverse holds too. If the request is pure CPU (generating a PDF, resizing images, heavy crypto), the advantage disappears and a risk shows up: synchronous CPU blocks the event loop for every in-flight request in that environment. The docs tell you to use worker_threads or lower maxConcurrencyPerEnvironment. That's the Bulkhead pattern applied inside the process: you cap how many requests share the same resource so one slow request doesn't sink the rest.
The trade-offs (it's not all sunshine and rainbows)
Shared state across requests
One request per environment forgave a lot of sins. The official best practices forbid credentials, customer data, response objects or mutable request state in global variables. Connection pools and SDK clients can stay global, as long as you validate stale connections and cap caches. And the 512 MB /tmp is shared across requests, so file names have to be unique.
Node.js only, Arm64 only
At launch it's nodejs24.x only, Arm64 only. Native modules compiled for x86_64 won't run. There are no runtime management controls: you can't pin a patch version. Java, Python or .NET teams stay on Event Functions (or MicroVMs).
Fixed size
Every environment gets 2 GB of memory, no choice, split between heap, buffers, native modules and every in-flight request. The default V8 heap is 1,024 MB; you can raise it with NODE_OPTIONS=--max-old-space-size=..., as long as you leave room for everything else. On Event Functions you pick anything from 128 MB to 10 GB.
Pricier requests
$0.30 per million, versus $0.20 on Event Functions: 50% more. In high-request, low-CPU workloads, the request line adds up: in the official auth middleware example (100 million requests), $30.00 of the $64.56 total is requests alone.
The fine print
HTTP/1.1 only (HTTP/1.0 gets a 505), the Upgrade header is stripped (WebSocket doesn't show up as supported in the docs), and the service metrics have no revision dimension. During a canary, you log the revision ID yourself in the application.
Let's run the numbers (with what's confirmed)
I'll use the official I/O-heavy API example from the pricing page, because every assumption comes from AWS: us-east-1, Arm, 10 million requests a month, 200 ms end to end, 50 ms of active CPU per request, a single environment serving everything for 30 days (2,592,000 seconds), 0.29 GB of memory consumed, no free tier and no data transfer.
- CPU: 10,000,000 × 0.05 s = 500,000 vCPU-seconds × $0.0000276944 = $13.85
- Memory: 0.29 GB × 2,592,000 s × $0.0000036667 = $2.76
- Requests: 10 × $0.30 = $3.00
- Total: $19.61
Memory is the open question. The runtime docs say the environment has a fixed 2 GB, and the example uses 0.29 GB. The pricing page settles half of it, because it talks about memory consumed by the environment. It doesn't explain how that consumption is measured (peak, average, sampling). If it were the full 2 GB, the memory line in this example would jump to about $19 and the total would nearly double. That's why I'm not handing you an Event versus Web comparison built on made-up numbers. Measure before you decide:
- CPU time per request versus total duration (today's
Durationon your Event Function is the ceiling). - Process memory under the concurrency you plan to run, compared with the memory line on the bill.
- How many environments stay alive throughout the day, because memory is billed per live environment, even idle ones.
- Data transfer, which is left out of every official example and weighs heavily in the cross-region scenario in the next section.
This ties straight into the Cost Optimization pillar of the Well-Architected Framework: measure real demand before picking the pricing model.
The gap: no South America region at launch
The launch regions are us-east-1, us-east-2, us-west-2, ap-northeast-1 (Tokyo) and eu-west-1 (Ireland). Nothing in South America, so São Paulo (sa-east-1) is out, and I couldn't find any official timeline for when it comes in. Brazil is the concrete case I deal with every day, but the same reasoning applies to anyone whose users and data live far from those five regions. If that's you, the question stops being "Web or Event?" and becomes "is it worth crossing a continent to use Web Functions?".
The path AWS left ready is CloudFront. On the same day, CloudFront shipped support for Web Functions as an origin, with Origin Access Control and at no additional cost. The configuration confirmed in the docs:
- Endpoint with
AuthType: IamAuth. - OAC with origin type
lambdaand signing behavioralways-amz-auth(CloudFront rejects the others). - A resource-based policy on the Web Function allowing
lambda:InvokeWebFunctionEndpointfor thecloudfront.amazonaws.comprincipal, withaws:SourceArnpinned to your distribution.
{
"Principal": { "Service": "cloudfront.amazonaws.com" },
"Action": "lambda:InvokeWebFunctionEndpoint",
"Condition": {
"StringEquals": {
"aws:SourceArn": "arn:aws:cloudfront::111122223333:distribution/<distribution-id>"
}
}
}
On the Security side, the endpoint domain is still reachable from the internet even behind CloudFront. With IamAuth, only requests signed by someone your policies allow get through; with ApplicationManaged, anyone who knows the domain can call your application. To lock that down across the organization, the docs show an SCP using the lambda:Request/WebFunctionAuthType condition key to deny ApplicationManaged.
Where your data lives drives the architecture. CloudFront brings the edge closer, but the function is still in the United States. If your database is in sa-east-1, every query from your function crosses the continent, and an SSR page with five queries pays that trip five times before returning a single byte, with inter-region data transfer on the bill. There's no official latency number for this, so measure it: the time of one query from us-east-1 to your database, times the number of queries on the route.
There's also a networking limit: the egress network connector, which gives you access to your VPC, only works with a HomeRegion endpoint. It doesn't give you internet access (you need a NAT gateway). Without a connector, the egress IP is managed by Lambda and is no good for an allow list; with a connector, the docs tell you to allow the destination by security group or by the subnets' CIDR, because there's no stable source IP either.
Here's the decision table:
| Scenario | Recommendation |
|---|---|
| I/O-bound API or SSR, data can live in us-east-1, no requirement to keep data in Brazil (from a customer contract or your reading of LGPD, Brazil's data protection law, similar to GDPR) | Web Functions in us-east-1 + CloudFront with OAC, data in the same region |
| Database has to stay in sa-east-1 (contract, compliance, latency to other systems) | Stay on Event Functions in sa-east-1, behind API Gateway or a Function URL |
| CPU-bound workload, queues, file uploads | Event Functions, in any region |
| Java, Python or .NET stack | Event Functions (or MicroVMs) |
| Authorizer or middleware with lots of requests and little CPU | Run the numbers: the 50% pricier request can eat the CPU savings |
| Lots of global state in the code, no time to refactor | Stay on Event Functions until the global scope is clean |
| Need VPC access with a multi-region endpoint, or an egress IP for an allow list | Event Functions; on Web Functions, VPC only with a HomeRegion endpoint + connector (and NAT for internet), and validate the egress IP before promising an allow list |
I covered this kind of region trade-off, in terms of cost and availability, in high availability has a price. And concentrating everything in us-east-1 carries its own risk, as I explained in the post on why multi-cloud can make you less resilient. From a Reliability standpoint, leaving your home region means trading one dependency for another, so choose with your eyes open.
What you take away
- Web Functions runs your Node.js HTTP server on Lambda with no adapter, with up to 128 requests per environment (default 64), billing for active CPU, memory consumed while the environment is alive, and $0.30 per million requests.
- The win is in I/O-bound workloads: SSR, APIs that wait on a database, LLM streaming. CPU-bound work, queues and uploads stay on Event Functions.
- The price is discipline (zero request state in global variables, no synchronous CPU on the event loop) plus accepting a fixed 2 GB and Node.js 24 on Arm64 only.
- Before comparing costs, measure CPU per request, consumed memory and live environments (the docs don't detail how billed memory is measured).
- With no South America region, the real decision for a Brazilian workload is: data in us-east-1 with CloudFront + OAC in front, or stay on Event Functions in São Paulo. With the database on the other side of the continent, per-query latency can eat whatever you gained by switching.
Sources
- What's New: AWS Lambda Web Functions
- AWS News Blog: Announcing AWS Lambda Web Functions
- AWS Compute Blog: Exploring AWS Lambda Web Functions
- Developer Guide: Lambda Web Functions
- Best practices
- Endpoint access control
- Networking
- AWS Lambda Pricing
- What's New: CloudFront with Lambda Web Functions as an origin
- CloudFormation: AWS::Lambda::WebFunction
Originally published on willpeixoto.dev.
So, are you running SSR or a Node API on Lambda Web Adapter today? Would you move it to a launch region behind CloudFront, or hold it close to your users until a nearby region shows up? Tell me in the comments, I want to take this conversation to the next AWS User Group São Paulo meetup. If this post helped, drop a like and share it around. Thanks a lot!
BUILD. SCALE. REPEAT. =D

Top comments (0)