Why I'm Excited About AWS Lambda Web Functions

serverlesslambdaweb-functionsAWS

Full disclosure: I work for AWS, with the serverless team. I’ve been watching this come together, and I’ve been excited for quite a while.

AWS just released AWS Lambda Web Functions! Check out the release blog, the Compute blog deep dive, and the developer guide for the how-to of it. This post is pattern-driven: one section per protocol or framework, each with its own small example.

The real thing to look at is the repo: lambda-web-function-examples on GitHub has working, deployable code for every pattern below, REST, SSE, chunked streaming, Connect/gRPC-Web RPC, the webhook receiver, IAM auth, and Express/Fastify. This post is the companion walkthrough. Clone the repo and run the examples yourself; come back here for the why behind each one.

A new shape

Lambda now has three form factors, all built on the same Firecracker isolation. Event Functions are the Lambda you already know, the event-driven handler model from the last ten years. Last year that model got durable execution, letting a function checkpoint and resume a long-running step. This year added MicroVMs for running untrusted or user-generated code with a lifecycle you manage yourself.

Web Functions is the newest form factor, and a genuinely different shape from all three. Instead of a handler that reacts to an event, you write a standard HTTP server that listens on a port, the same server you’d run locally, in a container, or on a VM. Lambda runs it, scales it, and gives it a public HTTPS endpoint. The execution environments underneath are still the same ephemeral, Firecracker-isolated ones Lambda has always used. Nothing here is long-lived compute: Lambda is still deciding when to spin an environment up and tear it down, Web Functions just change what runs inside it from a one-shot handler to a listening server.

What I want to spend this post on is breadth: not one app shown in depth, but a short tour across the different protocols and frameworks that all land on the same shape. If you’ve seen the SSE dashboard example in the Compute blog, this is the same platform, pointed at several other patterns side by side: REST, RPC, webhooks, IAM-protected endpoints, and a couple of frameworks running exactly as they would anywhere else.

Standard REST

REST is the most common way computers talk to each other today, so let’s start here. A plain HTTP/1.1 request/response server, built here with Hono:

const { serve } = require('@hono/node-server');
const { Hono } = require('hono');

const app = new Hono();

app.get('/items/:id', (c) =>
  c.json({ id: c.req.param('id'), name: `item-${c.req.param('id')}` })
);

app.post('/echo', async (c) => c.json({ youSent: await c.req.json() }));

const [hostname, portStr] = (process.env.AWS_LAMBDA_HTTP_ENDPOINT || 'localhost:3000').split(':');
serve({ fetch: app.fetch, hostname, port: Number(portStr) });

Routes, path params, a JSON body: nothing here is Web-Functions-specific except reading the host and port from AWS_LAMBDA_HTTP_ENDPOINT. Swap in any other Node framework, and the only thing that changes is which function you pass that host and port to.

Server-Sent Events

Where REST is one request in, one response out, SSE holds the connection open instead and pushes events as they’re produced. It’s nice to see that born out on the wire itself. Real gaps show up between frames, not one response buffered and delivered all at once:

GET /events?n=5&delay=200
event: tick   (t=0ms)
event: tick   (t=204ms)
event: tick   (t=405ms)
event: tick   (t=606ms)
event: tick   (t=807ms)
event: done

That’s the pattern for live feeds, progress updates, or pushing LLM tokens to a browser as they’re generated. It’s the same underlying behavior the dashboard example in the Compute blog is built around, just isolated here as its own small case.

SSE earns its keep beyond “it streams,” too. If you’re building an MCP server, SSE is one of its primary transports: tool calls and results flow back to the client as a stream of discrete events rather than one blocking response, which matters when a tool call can take a while or needs to emit progress along the way. SSE also rides on plain HTTP, so there’s no protocol upgrade. It works through the same proxies, load balancers, and auth middleware as any other request, and the browser’s EventSource reconnects automatically on a dropped connection without you writing that logic yourself. For strictly one-directional, server-to-client delivery, which covers most streaming use cases including LLM output and MCP tool results, that’s a simpler, more infrastructure-friendly fit than a protocol like WebSockets that pays for bidirectionality you don’t need.

Chunked streaming

A close cousin of SSE: raw chunked HTTP streaming (Transfer-Encoding: chunked), useful for NDJSON (one JSON record per line) or plain incremental text, where you don’t need the event:/data: framing SSE expects. Each write() reaches the client as it happens:

GET /ndjson?n=5&delay=200
{"i":1,"elapsedMs":0}
{"i":2,"elapsedMs":202}
{"i":3,"elapsedMs":403}
{"i":4,"elapsedMs":603}
{"i":5,"elapsedMs":803}

Both streaming patterns are bounded by the function’s request timeout, up to 900 seconds. There’s no infinite stream, but there’s real, incremental delivery for as long as the request runs.

Line-delimited JSON over plain HTTP works well for a client that doesn’t speak EventSource. Think a CLI tool, a backend-to-backend integration, or a batch job streaming per-record results, like search hits or export rows, as they’re produced instead of waiting for the whole set to finish before sending anything back.

RPC with Connect and gRPC-Web

A pattern that doesn’t come up in a REST-and-SSE walkthrough: a single Connect server handling both the Connect protocol and gRPC-Web, unary and server-streaming calls, auto-negotiated from the request’s Content-Type. One server, two wire protocols:

curl -X POST https://$DOMAIN/greet.v1.GreeterService/Greet \
  -H 'Content-Type: application/json' -d '{"name":"world"}'
# -> {"greeting":"Hello, world!"}

Native gRPC and bidirectional streaming need HTTP/2, and the edge speaks HTTP/1.1, so those two shapes aren’t available here. Unary and server-streaming still cover a lot of real RPC use cases without needing them.

The pattern works best for service-to-service calls where you want a strongly-typed contract, a .proto schema generating client and server code in multiple languages, instead of hand-maintained REST shapes, but you also need a browser or a plain fetch call to reach the same endpoint without a gRPC-aware proxy in front of it. Connect gives you one server that both your internal services and a web client can call directly.

Signature-verified webhooks

A webhook receiver is a good example of what it means for your code to own the whole request. There’s no platform-level signature checking here. The handler reads the raw body, verifies an HMAC-SHA256 signature against it in the GitHub/Stripe style with a timing-safe comparison, and rejects anything that doesn’t match before trusting the payload:

POST /webhook   (correct signature)   -> 200 {"ok":true,"received":{...}}
POST /webhook   (wrong signature)     -> 401 {"error":"invalid signature"}
POST /webhook   (no signature header) -> 401 {"error":"invalid signature"}

Testing the rejection paths matters as much as the happy path. It’s the part that actually proves the verification is enforced rather than just present in the code.

HMAC-signed webhooks are the default pattern across most SaaS platforms that push events to your endpoint. GitHub, Stripe, Slack, Shopify, and Twilio all sign their webhook payloads this way, since there’s no other platform-level way for your server to know the request actually came from them and not from anyone who guesses the URL.

IAM-authenticated endpoints

The webhook pattern uses ApplicationManaged auth, where every request reaches your app and your code decides what to trust. The other mode, IamAuth, moves that decision to the platform. The request must be SigV4-signed, and an unsigned request never reaches your handler at all:

GET /   (no auth)                  -> 403 Forbidden
GET /   (SigV4-signed request)     -> 200 {"service":"iam-auth", ...}

Useful for service-to-service calls or internal APIs where you’d rather not write any auth logic into the app itself.

IAM auth works for any caller holding valid AWS credentials for a principal allowed to call the endpoint, not just AWS resources acting under a role. That’s usually another AWS resource (a Lambda function, an EC2 instance, a Step Functions state), but it can just as well be a browser using temporary credentials from a Cognito identity pool, or a third-party partner you’ve issued an IAM user and scoped permissions to. What it rules out is an anonymous caller: nobody without AWS credentials for an allowed principal gets past the signature check, which is what makes this mode a good fit for internal service-to-service calls or for exposing an endpoint to a specific, credentialed partner rather than the open internet.

Frameworks that just work

The pattern underneath everything above holds for ordinary frameworks too, because Web Functions isn’t a new runtime model to adopt. It’s a server listening on a port. Express and Fastify both run completely unmodified:

// Express, no Lambda-specific code anywhere
const express = require('express');
const app = express();
app.use(express.json());

app.get('/items/:id', (req, res) => res.json({ id: req.params.id }));

const [hostname, portStr] = (process.env.AWS_LAMBDA_HTTP_ENDPOINT || 'localhost:3000').split(':');
app.listen(Number(portStr), hostname);

Routing, middleware, and JSON body parsing all work exactly as they do anywhere else you’d run that same app.

The thread connecting all of them

The only thing any of these seven patterns do differently from running locally is read AWS_LAMBDA_HTTP_ENDPOINT for the host and port to listen on. Everything else, the routing, the streaming, the RPC negotiation, the signature verification, is just the server doing what it would do anywhere. The platform adapted to these patterns, not the other way around.

What I didn’t get into

I kept this one narrow on purpose: one shape, a bunch of patterns. Everything else about Web Functions is already covered well elsewhere, so go there instead of reading my summary of it:

  • Deploying, and what a revision/endpoint actually is, how aws lambda-web deploy works, in the AWS announcement.
  • Concurrency, scale-out, and active-CPU billing, with a real CloudWatch Logs Insights query against a load test, in the Compute blog walkthrough.
  • Going multi-Region with latency-based routing behind one global URL, also in the Compute blog walkthrough.
  • Canary and blue-green rollouts via revision-weights, and CloudFront/OAC for a custom domain or WAF in front of an endpoint, both in the developer guide.
  • Everything else, runtimes, networking/VPC, lifecycle, permissions, encryption, monitoring, troubleshooting, is indexed from that same developer guide.

The repo is on GitHub, with working code for every pattern above. This post is the companion.

← Back to blog