Exploring Lambda Web Functions with an SSE Demo
This article is a machine translation of the contents of the following URL, which I wrote in Japanese:
{% embed qiita.com %}
Introduction
Hi, I'm Houkiboshi @H0ukiStar.
On October 12, 2026, Lambda Web Functions became generally available. It's a new type of Lambda function whose code can receive HTTP requests directly.
{% embed aws.amazon.com %}
In this article, I'll first give an overview of Lambda Web Functions, then use an SSE-based demo to compare how "a single environment handles multiple requests concurrently" against a traditional Lambda.
> Note: This article is based on a talk I gave at JAWS-UG Sainokuni Saitama Branch #12, rewritten as an article.
> Exploring Lambda Web Functions
What Are Lambda Web Functions?
Lambda Web Functions are a new kind of Lambda function whose code can receive HTTP requests directly. A lot of the hassle that used to come with running a web framework on Lambda is now gone.
Here are the key characteristics:
- You can run Express / Hono / plain Node.js and others with no adapter, as-is
- Deployment is a single command: aws lambda-web deploy
- A single environment handles up to 128 (default 64) requests concurrently
- The only runtime currently available is Node.js 24
- The execution environment is currently fixed at Arm64 / 2 vCPU / 2 GB
> Note: The runtime and execution environment specs are the GA-time specification and may change in future updates.
> Check the official documentation for the latest support status.
On the application side, the only thing you need to be aware of is starting your HTTP server on the listen target that the runtime passes to you.
// The runtime passes the listen target via the environment variable AWS_LAMBDA_HTTP_ENDPOINT
const [host, port] = process.env.AWS_LAMBDA_HTTP_ENDPOINT.split(':');
// Just start an ordinary HTTP server using the listen target you received
app.listen(port, host);
So all you do is write your usual web server and listen as-is, and it works.
Existing Lambda Renamed to "Event Functions"
Alongside the arrival of Lambda Web Functions, the traditional Lambda has been renamed to Event Functions (a name-only change; the behavior is unchanged).
Since traditional Lambda follows a model where it receives an event (a dictionary) and processes it, I personally find the naming fitting.
Previously, the Lambda Web Adapter would convert an event like the one below into an HTTP request.
export const handler = async (event) => {…}
// Example event from a function URL (partially omitted)
{
"requestContext": {
"http": {
"method": "GET",
"path": "/"
}
},
"rawPath": "/",
"rawQueryString": "",
"headers": {…},
"isBase64Encoded": false
}
With Web Functions, you no longer need to be aware of this kind of event conversion.
A Three-Component Architecture
Lambda Web Functions consist of three components. It's easier to understand by mapping them to Event Functions concepts.
| Component | Role | Equivalent in Event Functions |
| --- | --- | --- |
| Web Functions | The top-level container that has a name and an Arn | The function itself |
| Endpoint | The entry point that routes HTTP traffic to a Revision | Alias |
| Revision | An immutable snapshot that holds code + configuration; increases with each deploy | Function version |
The fact that a Revision is an immutable snapshot that stacks up with each deploy is exactly the image of a function version.
Here's a diagram of how these three components relate to each other.
!Web Functions / Endpoint / Revision architecture overview
Pricing
There are three billing elements.
- Requests: $0.30 / 1M requests
- Memory: $0.0000042688 / GB-second
- Active vCPU: $0.0000322421 / vCPU-second
The key point is that you're billed for active CPU time. As a result, wait time for things like DB access, external API calls, and AI inference is not billed.
Note that this is specifically about CPU; memory behaves a bit differently. While CPU is billed only for the time it's active, it's worth keeping in mind that memory is billed continuously for as long as the execution environment exists, including the gaps between requests.
> You pay for requests, the CPU time your code actively uses, and the memory consumed by each execution environment for as long as it runs.
{% embed aws.amazon.com %}
For reference, the Event Functions pricing is as follows.
- Requests: $0.20 / 1M requests
- Memory: $0.0000133334 / GB-second
If you just line up the numbers, the Event Functions memory unit price (GB-second) looks higher than Web Functions, but this isn't a straightforward comparison. Event Functions use a pricing model where CPU and other resources are allocated in proportion to the amount of memory you assign, so the CPU cost is effectively included in the memory price. That means you can't make an apples-to-apples comparison based on memory unit price alone.
> Warning: All of the above are reference prices for the Tokyo region, and additional data transfer costs and the like apply.
> Always check the official pricing page for the latest and accurate pricing.
Trying Out Lambda Web Functions
This is the main topic of the article. Let's actually verify that "a single environment can process multiple requests."
This time I built a demo that observes multiple sustained connections using SSE (Server-Sent Events). It keeps the connections open and pushes "how many connections this environment is currently holding" from the server to the client every 2 seconds.
!Demo observing concurrent connections via SSE
I prepared this for both Event Functions (Lambda Web Adapter setup) and Web Functions and compared their behavior.
The full set of sample code used in the demo is available in the repository below; please refer to it for details.
{% embed H0ukiStar/sample-aws-lambda-web-functions %}
Demo Setup
The repository has samples that implement the same SSE demo in two different ways.
| Directory | Approach | Overview |
| --- | --- | --- |
| web-functions-sse | Lambda Web Functions | No adapter. A standard Node.js HTTP server just reads AWS_LAMBDA_HTTP_ENDPOINT and listens. Deployed with aws lambda-web deploy |
| event-functions-web-adapter-sse | Event Functions + Lambda Web Adapter | A traditional Lambda with Lambda Web Adapter added as a layer. Deployed with AWS SAM |
Both samples are simple, with the following three endpoints.
- GET / … A dashboard that visualizes the connection status (open it in a browser)
- GET /events … The SSE stream. Pushes instanceId and activeConnections every 2 seconds
- GET /status … Returns the current instanceId / activeConnections as JSON
When you increase connections in parallel from the dashboard, with Web Functions you'll see the instanceId stay the same while activeConnections increases (i.e., one environment is shared), whereas with Event Functions the instanceId is always different for each connection (i.e., one environment = one request). This is the biggest difference between the two.
> Warning: Both samples expose endpoints with no authentication, as a public configuration. Keep them to evaluation use, and delete them once you no longer need them.
The Event Functions Case
First, the demo with the traditional Event Functions (LWA). Here's what it looks like as I actually increase the connections.
!Event Functions (LWA) demo
- Concurrent connections per environment: 1
- Connections opened in parallel always get entirely different instanceIds, and each environment's activeConnections is always 1
- Maximum number of environments: The upper limit of requests it can handle concurrently depends on the account's concurrency
In other words, it's one request = one environment, and you can see that each additional connection spins up a separate environment.
The Web Functions Case
Next, the demo with Web Functions. When you increase connections the same way, you see different behavior from before.
!Web Functions demo
- Concurrent connections per environment: The Revision setting maxConcurrencyPerEnvironment becomes the maximum number of requests that can be accepted concurrently
- In the demo it's set to 2, so each environment's activeConnections caps at 2, but you can confirm that a single environment handles multiple requests
- Maximum number of environments: The Endpoint setting Maximum environments becomes the maximum number of environments
- In the demo it's set to 2, so from the 5th connection onward, a 429 is returned
> Note: What's returned in this demo is a 429 Too Many Requests, indicating that the concurrency limit has been reached. Separately, a 503 Service Unavailable may be returned when, for example, an environment hasn't started yet and can't take on the processing. When you implement retries on the client side, it's reassuring to distinguish and handle these two separately.
Whereas Event Functions had one environment per connection, with Web Functions a single environment can hold multiple connections together. That difference came through clearly.
Differences in Environment Lifecycle
If we organize this difference from the perspective of the environment lifecycle, it comes out like this.
- Event Functions: One environment = one request
- When multiple requests arrive simultaneously, environments cold-start up to the concurrency limit
!Event Functions environment lifecycle (one environment = one request)
- Web Functions: A single environment processes multiple requests concurrently
- It can process a default of 64, up to a maximum of 128, concurrently
- This reduces cold starts accordingly
!Web Functions environment lifecycle (one environment processes multiple requests concurrently)
The image is that when requests spike, Event Functions spins up environment after environment to handle them, while Web Functions handles multiple requests together in a single environment.
As a result, cold starts occur less often.
Conclusion
This time, I introduced Lambda Web Functions through an overview and a comparison of concurrent connections via an SSE demo.
To summarize the key points:
- A Lambda that listens for HTTP directly, removing the need for the Lambda Web Adapter that Event Functions used to require
- A single environment can process multiple requests concurrently, reducing cold starts accordingly
- Because billing is based on active CPU, it's resilient to I/O waits and pairs well with SSE / streaming workloads
In particular, the two points of "one environment can handle multiple requests" and "no billing during wait time" pair well with web applications and workloads that involve external APIs or AI inference. If you actually migrate your own application with aws lambda-web deploy, I think you'll find it interesting to feel the difference in behavior firsthand.
I hope this article helps when you try out Lambda Web Functions.
1 reaction
AWS Lambda now runs your HTTP server. Do you still need API Gateway?
AWS recently announced AWS Lambda Web Functions, a new kind of Lambda function that runs your code as a Node.js HTTP server listening on a port, rather than as a handler receiving an event. To tell the two apart, the Lambda functions you already know are now called Event Functions. AWS says Web Functions suit web applications, API backends and other HTTP workloads with many concurrent requests.
Until now, the usual ways to run an API on Lambda have been these:
- One function per route behind API Gateway.
- The whole app in one function behind an adapter, the Lambdalith.
- A function URL in front of one function, with no API Gateway at all.
The adapters made that work by turning every event back into a request your framework understands, but you were left with one request at a time per environment, billing for the time spent waiting on I/O and a 6 MB payload limit, which is why Aidan Steele, AWS Hero, calls the approach "always a hack". Even AWS's own Lambda Web Adapter now recommends Web Functions for new Node.js web apps.
The one Web Functions-specific detail is the address your server listens on, which Lambda passes as a host and port in the AWS_LAMBDA_HTTP_ENDPOINT environment variable, as in the Express example from the announcement:
import express from 'express';
const app = express();
const [host, port] = process.env.AWS_LAMBDA_HTTP_ENDPOINT.split(':');
app.get('/', (req, res) => {
res.json({ message: 'Hello from Lambda Web Functions!' });
});
app.listen(port, host, () => {
console.log(`Server running on port ${port}`);
});
<figcaption>
From <a href="aws.amazon.com">Announcing AWS Lambda Web Functions</a>, AWS News Blog.
</figcaption>
Because a Web Function is a plain Node.js server, local development and testing are simpler than with an Event Function behind API Gateway, where local testing can take an emulator such as sam local start-api, running your functions in a local Docker container. On your laptop nothing sets the variable, so you set it yourself and run the same index.js with Node:
AWS_LAMBDA_HTTP_ENDPOINT=127.0.0.1:3000 node index.js
What changes in practice is that one environment handles up to 128 requests at once, 64 by default, so a burst of traffic shares warm environments instead of starting one per request. Lambda bills compute for the CPU time your code actually uses, so a request waiting on a database or another API uses little of the CPU you pay for.
One caveat: anything your code keeps in a module-level variable is shared by every request in that environment, so a value meant for one request can end up in another. The docs are clear that your code must be safe for concurrent use.
Web Functions make me wonder whether it's time to rethink the Lambdalith, which we've long treated as a pattern to avoid. AWS's own Lambda guide lists it among anti-patterns that "work but can increase costs and complexity", and Yan Cui, AWS Serverless Hero, covered its trade-offs and adopted it "where it makes sense". On his podcast, in a conversation recorded before Web Functions existed, he weighed a tailored IAM role on each function and lighter cold starts against how useful one function was for bringing an existing Express app across. His guest, Heitor Lessa, who created Powertools for AWS Lambda while at AWS, said he usually started from one function and split it as the boundaries and the permissions became clear.
Web Functions soften the cold-start side of the case for splitting, since environments start less often, but not the permissions side, because every route runs under the same execution role. Still, a Node.js server we'd have put in a container on Fargate could now run as a single Web Function, which makes the Lambdalith look less like an anti-pattern and more like a choice with its own trade-offs.
An API Gateway REST API checks API keys, holds each customer to a usage plan and throttles a noisy neighbour without slowing everyone else. A Web Function comes with its own HTTPS endpoint, which does none of that, and its rate limit applies to the whole endpoint in each Region. That leaves the question of whether you still need API Gateway.
What goes in front
If you need a custom domain, AWS WAF or caching, AWS recommends putting CloudFront or API Gateway in front of the endpoint, since the endpoint itself doesn't support custom domain names or direct AWS WAF association. The endpoint's domain stays reachable from the internet, so clients can go around whatever you put in front unless the endpoint refuses them:
!Requests reaching a Web Function endpoint directly, through CloudFront or through API Gateway
Nothing in front
Each endpoint has an authentication type, a setting that controls which requests reach your code. The default, ApplicationManaged, lets every request through to your code, while IamAuth accepts only requests signed with SigV4 by an IAM identity allowed to invoke the endpoint. An IamAuth endpoint suits calls between services inside AWS, where every client already has an IAM identity, as long as they can share the endpoint's rate limit. Lambda doesn't add CORS headers for Web Functions, so your app answers preflight requests itself.
CloudFront
CloudFront is another option that lets you make the endpoint refuse requests that don't come through your distribution, using origin access control on an IamAuth endpoint. CloudFront signs every request it sends, though requests signed by any IAM identity your policies allow still get through. To let CloudFront in, you attach this resource-based policy from AWS's CloudFront guide, with aws lambda-web put-resource-policy on the function's ARN, not the endpoint's:
{
"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-function/endpoint/prod",
"Condition": {
"StringEquals": {
"aws:SourceArn": "arn:aws:cloudfront::111122223333:distribution/EDFDVBD6EXAMPLE"
}
}
}
]
}
<figcaption>
From <a href="docs.aws.amazon.com">Restrict access to an AWS Lambda Web Function origin</a>, Amazon CloudFront Developer Guide.
</figcaption>
Your app can still check its own tokens, such as a JWT, because CloudFront adds its signature in a dedicated header and passes the viewer's Authorization header to the origin unmodified, as long as the header is in the cache policy. That also puts the header in the cache key, so with caching on, CloudFront keeps a separate copy of each response per token.
AWS WAF on the distribution can limit how often each client calls, by IP address or by a header value over a window of one to ten minutes, though AWS says it's not intended for precise request-rate limiting. A request that leaves the header out isn't evaluated or rate limited.
You give up Lambda@Edge origin request and origin response triggers on any cache behaviour that reaches the endpoint through origin access control. CloudFront's signature doesn't cover the body of PUT, POST and PATCH requests either, which the HTTPS connection to the endpoint protects in transit.
API Gateway
API Gateway reaches the endpoint through a plain HTTP proxy integration, since there's no Web Functions integration. Both REST and HTTP APIs have authorisers, but API keys, usage plans with a quota per day, week or month, request validation and AWS WAF need a REST API. Usage plan throttling and quotas are applied on a best-effort basis, so clients can sometimes go over them.
Neither the Lambda nor the API Gateway docs describe a way for API Gateway to sign requests to an IamAuth endpoint, so the endpoint stays ApplicationManaged. Lambda's best practices tell you to verify a shared secret or token in your app instead, and API Gateway can add the secret as a header on every request through parameter mapping.
Before you move your API
- Node.js 24 is the only runtime.
- Web Functions run exclusively on Arm64 and deploy from a .zip file, not a container image.
- Each environment gets 2 vCPUs and 2 GB, which you can't change.
- Endpoints accept HTTP/1.1 requests only, with no WebSocket upgrades.
Running it has a few gotchas:
- Lambda doesn't check that your server listens when you deploy, so a revision can show Active while every request fails.
- Web Function metrics don't include a revision dimension, so during a canary you can't compare the two revisions in those metrics.
- Outbound requests come from an address that isn't stable, so a partner's allow list needs an egress network connector into your VPC and a NAT gateway.
- On top of CPU time, you pay for memory over the life of each execution environment, idle time between requests included.
If your API isn't on Express, Eric Johnson, Principal Developer Advocate at AWS, has recently written a pattern-driven tour of seven protocols and frameworks on Web Functions, including server-sent events, gRPC-Web and signed webhooks, with working, deployable code for every pattern.
5 reactions · 2 comments
Mentions
Exploring Lambda Web Functions with an SSE Demo This article is a machine translation of the contents of the following URL, which I wrote in Japanese: {% embed qiita.com %} Introduction Hi, I'm Houkiboshi @H0ukiStar. On October 12, 2026, Lambda Web Functions became generally available. It's a new type of Lambda function whose code can receive HTTP requests directly. {% embed aws.amazon.com %} In this article, I'll first give an overview of Lambda Web Functions, then use an SSE-based demo to compare how "a single environment handles multiple requests concurrently" against a traditional Lambda. > Note: This article is based on a talk I gave at JAWS-UG Sainokuni Saitama Branch #12, rewritten as an article. > Exploring Lambda Web Functions What Are Lambda Web Functions? Lambda Web Functions are a new kind of Lambda function whose code can receive HTTP requests directly. A lot of the hassle that used to come with running a web framework on Lambda is now gone. Here are the key characteristics: - You can run Express / Hono / plain Node.js and others with no adapter, as-is - Deployment is a single command: aws lambda-web deploy - A single environment handles up to 128 (default 64) requests concurrently - The only runtime currently available is Node.js 24 - The execution environment is currently fixed at Arm64 / 2 vCPU / 2 GB > Note: The runtime and execution environment specs are the GA-time specification and may change in future updates. > Check the official documentation for the latest support status. On the application side, the only thing you need to be aware of is starting your HTTP server on the listen target that the runtime passes to you. // The runtime passes the listen target via the environment variable AWS_LAMBDA_HTTP_ENDPOINT const [host, port] = process.env.AWS_LAMBDA_HTTP_ENDPOINT.split(':'); // Just start an ordinary HTTP server using the listen target you received app.listen(port, host); So all you do is write your usual web server and listen as-is, and it works. Existing Lambda Renamed to "Event Functions" Alongside the arrival of Lambda Web Functions, the traditional Lambda has been renamed to Event Functions (a name-only change; the behavior is unchanged). Since traditional Lambda follows a model where it receives an event (a dictionary) and processes it, I personally find the naming fitting. Previously, the Lambda Web Adapter would convert an event like the one below into an HTTP request. export const handler = async (event) => {…} // Example event from a function URL (partially omitted) { "requestContext": { "http": { "method": "GET", "path": "/" } }, "rawPath": "/", "rawQueryString": "", "headers": {…}, "isBase64Encoded": false } With Web Functions, you no longer need to be aware of this kind of event conversion. A Three-Component Architecture Lambda Web Functions consist of three components. It's easier to understand by mapping them to Event Functions concepts. | Component | Role | Equivalent in Event Functions | | --- | --- | --- | | Web Functions | The top-level container that has a name and an Arn | The function itself | | Endpoint | The entry point that routes HTTP traffic to a Revision | Alias | | Revision | An immutable snapshot that holds code + configuration; increases with each deploy | Function version | The fact that a Revision is an immutable snapshot that stacks up with each deploy is exactly the image of a function version. Here's a diagram of how these three components relate to each other. !Web Functions / Endpoint / Revision architecture overview Pricing There are three billing elements. - Requests: $0.30 / 1M requests - Memory: $0.0000042688 / GB-second - Active vCPU: $0.0000322421 / vCPU-second The key point is that you're billed for active CPU time. As a result, wait time for things like DB access, external API calls, and AI inference is not billed. Note that this is specifically about CPU; memory behaves a bit differently. While CPU is billed only for the time it's active, it's worth keeping in mind that memory is billed continuously for as long as the execution environment exists, including the gaps between requests. > You pay for requests, the CPU time your code actively uses, and the memory consumed by each execution environment for as long as it runs. {% embed aws.amazon.com %} For reference, the Event Functions pricing is as follows. - Requests: $0.20 / 1M requests - Memory: $0.0000133334 / GB-second If you just line up the numbers, the Event Functions memory unit price (GB-second) looks higher than Web Functions, but this isn't a straightforward comparison. Event Functions use a pricing model where CPU and other resources are allocated in proportion to the amount of memory you assign, so the CPU cost is effectively included in the memory price. That means you can't make an apples-to-apples comparison based on memory unit price alone. > Warning: All of the above are reference prices for the Tokyo region, and additional data transfer costs and the like apply. > Always check the official pricing page for the latest and accurate pricing. Trying Out Lambda Web Functions This is the main topic of the article. Let's actually verify that "a single environment can process multiple requests." This time I built a demo that observes multiple sustained connections using SSE (Server-Sent Events). It keeps the connections open and pushes "how many connections this environment is currently holding" from the server to the client every 2 seconds. !Demo observing concurrent connections via SSE I prepared this for both Event Functions (Lambda Web Adapter setup) and Web Functions and compared their behavior. The full set of sample code used in the demo is available in the repository below; please refer to it for details. {% embed H0ukiStar/sample-aws-lambda-web-functions %} Demo Setup The repository has samples that implement the same SSE demo in two different ways. | Directory | Approach | Overview | | --- | --- | --- | | web-functions-sse | Lambda Web Functions | No adapter. A standard Node.js HTTP server just reads AWS_LAMBDA_HTTP_ENDPOINT and listens. Deployed with aws lambda-web deploy | | event-functions-web-adapter-sse | Event Functions + Lambda Web Adapter | A traditional Lambda with Lambda Web Adapter added as a layer. Deployed with AWS SAM | Both samples are simple, with the following three endpoints. - GET / … A dashboard that visualizes the connection status (open it in a browser) - GET /events … The SSE stream. Pushes instanceId and activeConnections every 2 seconds - GET /status … Returns the current instanceId / activeConnections as JSON When you increase connections in parallel from the dashboard, with Web Functions you'll see the instanceId stay the same while activeConnections increases (i.e., one environment is shared), whereas with Event Functions the instanceId is always different for each connection (i.e., one environment = one request). This is the biggest difference between the two. > Warning: Both samples expose endpoints with no authentication, as a public configuration. Keep them to evaluation use, and delete them once you no longer need them. The Event Functions Case First, the demo with the traditional Event Functions (LWA). Here's what it looks like as I actually increase the connections. !Event Functions (LWA) demo - Concurrent connections per environment: 1 - Connections opened in parallel always get entirely different instanceIds, and each environment's activeConnections is always 1 - Maximum number of environments: The upper limit of requests it can handle concurrently depends on the account's concurrency In other words, it's one request = one environment, and you can see that each additional connection spins up a separate environment. The Web Functions Case Next, the demo with Web Functions. When you increase connections the same way, you see different behavior from before. !Web Functions demo - Concurrent connections per environment: The Revision setting maxConcurrencyPerEnvironment becomes the maximum number of requests that can be accepted concurrently - In the demo it's set to 2, so each environment's activeConnections caps at 2, but you can confirm that a single environment handles multiple requests - Maximum number of environments: The Endpoint setting Maximum environments becomes the maximum number of environments - In the demo it's set to 2, so from the 5th connection onward, a 429 is returned > Note: What's returned in this demo is a 429 Too Many Requests, indicating that the concurrency limit has been reached. Separately, a 503 Service Unavailable may be returned when, for example, an environment hasn't started yet and can't take on the processing. When you implement retries on the client side, it's reassuring to distinguish and handle these two separately. Whereas Event Functions had one environment per connection, with Web Functions a single environment can hold multiple connections together. That difference came through clearly. Differences in Environment Lifecycle If we organize this difference from the perspective of the environment lifecycle, it comes out like this. - Event Functions: One environment = one request - When multiple requests arrive simultaneously, environments cold-start up to the concurrency limit !Event Functions environment lifecycle (one environment = one request) - Web Functions: A single environment processes multiple requests concurrently - It can process a default of 64, up to a maximum of 128, concurrently - This reduces cold starts accordingly !Web Functions environment lifecycle (one environment processes multiple requests concurrently) The image is that when requests spike, Event Functions spins up environment after environment to handle them, while Web Functions handles multiple requests together in a single environment. As a result, cold starts occur less often. Conclusion This time, I introduced Lambda Web Functions through an overview and a comparison of concurrent connections via an SSE demo. To summarize the key points: - A Lambda that listens for HTTP directly, removing the need for the Lambda Web Adapter that Event Functions used to require - A single environment can process multiple requests concurrently, reducing cold starts accordingly - Because billing is based on active CPU, it's resilient to I/O waits and pairs well with SSE / streaming workloads In particular, the two points of "one environment can handle multiple requests" and "no billing during wait time" pair well with web applications and workloads that involve external APIs or AI inference. If you actually migrate your own application with aws lambda-web deploy, I think you'll find it interesting to feel the difference in behavior firsthand. I hope this article helps when you try out Lambda Web Functions.
AWS Lambda now runs your HTTP server. Do you still need API Gateway? AWS recently announced AWS Lambda Web Functions, a new kind of Lambda function that runs your code as a Node.js HTTP server listening on a port, rather than as a handler receiving an event. To tell the two apart, the Lambda functions you already know are now called Event Functions. AWS says Web Functions suit web applications, API backends and other HTTP workloads with many concurrent requests. Until now, the usual ways to run an API on Lambda have been these: - One function per route behind API Gateway. - The whole app in one function behind an adapter, the Lambdalith. - A function URL in front of one function, with no API Gateway at all. The adapters made that work by turning every event back into a request your framework understands, but you were left with one request at a time per environment, billing for the time spent waiting on I/O and a 6 MB payload limit, which is why Aidan Steele, AWS Hero, calls the approach "always a hack". Even AWS's own Lambda Web Adapter now recommends Web Functions for new Node.js web apps. The one Web Functions-specific detail is the address your server listens on, which Lambda passes as a host and port in the AWS_LAMBDA_HTTP_ENDPOINT environment variable, as in the Express example from the announcement: import express from 'express'; const app = express(); const [host, port] = process.env.AWS_LAMBDA_HTTP_ENDPOINT.split(':'); app.get('/', (req, res) => { res.json({ message: 'Hello from Lambda Web Functions!' }); }); app.listen(port, host, () => { console.log(`Server running on port ${port}`); }); <figcaption> From <a href="aws.amazon.com">Announcing AWS Lambda Web Functions</a>, AWS News Blog. </figcaption> Because a Web Function is a plain Node.js server, local development and testing are simpler than with an Event Function behind API Gateway, where local testing can take an emulator such as sam local start-api, running your functions in a local Docker container. On your laptop nothing sets the variable, so you set it yourself and run the same index.js with Node: AWS_LAMBDA_HTTP_ENDPOINT=127.0.0.1:3000 node index.js What changes in practice is that one environment handles up to 128 requests at once, 64 by default, so a burst of traffic shares warm environments instead of starting one per request. Lambda bills compute for the CPU time your code actually uses, so a request waiting on a database or another API uses little of the CPU you pay for. One caveat: anything your code keeps in a module-level variable is shared by every request in that environment, so a value meant for one request can end up in another. The docs are clear that your code must be safe for concurrent use. Web Functions make me wonder whether it's time to rethink the Lambdalith, which we've long treated as a pattern to avoid. AWS's own Lambda guide lists it among anti-patterns that "work but can increase costs and complexity", and Yan Cui, AWS Serverless Hero, covered its trade-offs and adopted it "where it makes sense". On his podcast, in a conversation recorded before Web Functions existed, he weighed a tailored IAM role on each function and lighter cold starts against how useful one function was for bringing an existing Express app across. His guest, Heitor Lessa, who created Powertools for AWS Lambda while at AWS, said he usually started from one function and split it as the boundaries and the permissions became clear. Web Functions soften the cold-start side of the case for splitting, since environments start less often, but not the permissions side, because every route runs under the same execution role. Still, a Node.js server we'd have put in a container on Fargate could now run as a single Web Function, which makes the Lambdalith look less like an anti-pattern and more like a choice with its own trade-offs. An API Gateway REST API checks API keys, holds each customer to a usage plan and throttles a noisy neighbour without slowing everyone else. A Web Function comes with its own HTTPS endpoint, which does none of that, and its rate limit applies to the whole endpoint in each Region. That leaves the question of whether you still need API Gateway. What goes in front If you need a custom domain, AWS WAF or caching, AWS recommends putting CloudFront or API Gateway in front of the endpoint, since the endpoint itself doesn't support custom domain names or direct AWS WAF association. The endpoint's domain stays reachable from the internet, so clients can go around whatever you put in front unless the endpoint refuses them: !Requests reaching a Web Function endpoint directly, through CloudFront or through API Gateway Nothing in front Each endpoint has an authentication type, a setting that controls which requests reach your code. The default, ApplicationManaged, lets every request through to your code, while IamAuth accepts only requests signed with SigV4 by an IAM identity allowed to invoke the endpoint. An IamAuth endpoint suits calls between services inside AWS, where every client already has an IAM identity, as long as they can share the endpoint's rate limit. Lambda doesn't add CORS headers for Web Functions, so your app answers preflight requests itself. CloudFront CloudFront is another option that lets you make the endpoint refuse requests that don't come through your distribution, using origin access control on an IamAuth endpoint. CloudFront signs every request it sends, though requests signed by any IAM identity your policies allow still get through. To let CloudFront in, you attach this resource-based policy from AWS's CloudFront guide, with aws lambda-web put-resource-policy on the function's ARN, not the endpoint's: { "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-function/endpoint/prod", "Condition": { "StringEquals": { "aws:SourceArn": "arn:aws:cloudfront::111122223333:distribution/EDFDVBD6EXAMPLE" } } } ] } <figcaption> From <a href="docs.aws.amazon.com">Restrict access to an AWS Lambda Web Function origin</a>, Amazon CloudFront Developer Guide. </figcaption> Your app can still check its own tokens, such as a JWT, because CloudFront adds its signature in a dedicated header and passes the viewer's Authorization header to the origin unmodified, as long as the header is in the cache policy. That also puts the header in the cache key, so with caching on, CloudFront keeps a separate copy of each response per token. AWS WAF on the distribution can limit how often each client calls, by IP address or by a header value over a window of one to ten minutes, though AWS says it's not intended for precise request-rate limiting. A request that leaves the header out isn't evaluated or rate limited. You give up Lambda@Edge origin request and origin response triggers on any cache behaviour that reaches the endpoint through origin access control. CloudFront's signature doesn't cover the body of PUT, POST and PATCH requests either, which the HTTPS connection to the endpoint protects in transit. API Gateway API Gateway reaches the endpoint through a plain HTTP proxy integration, since there's no Web Functions integration. Both REST and HTTP APIs have authorisers, but API keys, usage plans with a quota per day, week or month, request validation and AWS WAF need a REST API. Usage plan throttling and quotas are applied on a best-effort basis, so clients can sometimes go over them. Neither the Lambda nor the API Gateway docs describe a way for API Gateway to sign requests to an IamAuth endpoint, so the endpoint stays ApplicationManaged. Lambda's best practices tell you to verify a shared secret or token in your app instead, and API Gateway can add the secret as a header on every request through parameter mapping. Before you move your API - Node.js 24 is the only runtime. - Web Functions run exclusively on Arm64 and deploy from a .zip file, not a container image. - Each environment gets 2 vCPUs and 2 GB, which you can't change. - Endpoints accept HTTP/1.1 requests only, with no WebSocket upgrades. Running it has a few gotchas: - Lambda doesn't check that your server listens when you deploy, so a revision can show Active while every request fails. - Web Function metrics don't include a revision dimension, so during a canary you can't compare the two revisions in those metrics. - Outbound requests come from an address that isn't stable, so a partner's allow list needs an egress network connector into your VPC and a NAT gateway. - On top of CPU time, you pay for memory over the life of each execution environment, idle time between requests included. If your API isn't on Express, Eric Johnson, Principal Developer Advocate at AWS, has recently written a pattern-driven tour of seven protocols and frameworks on Web Functions, including server-sent events, gRPC-Web and signed webhooks, with working, deployable code for every pattern.