AWS Lambda Web Functions in practice: a Hono API, latency and cost
An API that spends most of its time waiting for a database has a different resource profile from a CPU-intensive worker. Evaluating serverless requires separating request duration, active processing and memory retained between requests.
Announced on October 12, 2026, AWS Lambda Web Functions introduces native HTTP execution: applications start a server, process concurrent requests and use a billing model based on active CPU, memory and requests. At launch, it supports Node.js 24 on Arm64 in Northern Virginia, Ohio, Oregon, Tokyo and Ireland. This article reflects documentation reviewed on October 13, 2026. (aws.amazon.com)
We will deploy a Hono API and design a reproducible evaluation. The examples are instructional, and the limits and acceptance criteria need tuning. No benchmark result is presented as an actual measurement.
1. How this differs from traditional Lambda
Web Functions run HTTP servers that listen on a network port. An execution environment can serve multiple requests concurrently, while the platform adds and removes environments with demand, including scaling to zero. There is no need to translate Lambda events into HTTP requests. (docs.aws.amazon.com)
Keep these models separate:
- Web Functions: native HTTP servers with their own revisions and endpoints.
- Event Functions: event-handler execution, still appropriate for queues and event-driven integrations.
- Function URLs with Hono: the documented Event Functions approach integrates with Lambda event payloads. That is not the deployment used here. (aws.amazon.com)
We use @hono/node-server to run Hono on Node.js HTTP. This is a Node.js runtime adapter, not a Lambda event adapter. (hono.dev)
The three core resources
- Web Function: the application resource.
- Revision: an immutable snapshot of code and configuration.
- Endpoint: an HTTPS entry point routing traffic to one or two revisions.
An Active resource state confirms control-plane readiness, not successful server startup. Always follow deployment with an HTTP smoke test. (docs.aws.amazon.com)
2. Architecture and a realistic use case
The lab request path is:
Client → HTTPS endpoint → Revision → Node.js environment → Hono routes
For a business API, the proposed production path is:
Client → CloudFront + AWS WAF → IamAuth endpoint → Hono → Dependencies
CloudFront provides the custom-domain layer and integration with AWS WAF, the web application firewall. The endpoint remains publicly reachable at the network layer: IAM restricts access but does not make it private. An outbound VPC connector does not change that exposure either. (docs.aws.amazon.com)
Problem: a team needs a catalog API with variable traffic without maintaining dedicated HTTP servers.
Proposed solution: begin with a single-region Web Function, validate behavior, then add persistence and geographic distribution if justified.
Implementation: a static catalog, a health route and a controlled asynchronous delay for experimentation.
Expected outcome: evidence about latency, concurrency and resource consumption before committing to an architecture. Neither savings nor performance improvements are assumed.
3. Prepare the environment and build the API
Use Node.js 24, npm, curl, temporary AWS credentials and AWS CLI v2 with the lambda-web command group. The deployment identity needs Web Functions permissions and access to supporting resources, including iam:PassRole where required. (docs.aws.amazon.com)
aws --version
aws lambda-web help
aws sts get-caller-identity
export AWS_REGION=us-east-1
mkdir hono-web-api
cd hono-web-api
npm init -y
npm pkg set type=module
npm pkg set scripts.start='node index.js'
npm install --save-exact hono @hono/node-serverCommit package.json and package-lock.json. In the pipeline, install with npm ci --omit=dev and test the exact deployment package. If native dependencies are introduced, build for Linux Arm64 rather than reusing binaries from an x86 workstation. (docs.aws.amazon.com)
Create index.js:
import { Hono } from 'hono';
import { serve } from '@hono/node-server';
import { randomUUID } from 'node:crypto';
import { performance } from 'node:perf_hooks';
import { setTimeout as delay } from 'node:timers/promises';
const app = new Hono();
const bootId = randomUUID();
const build = process.env.APP_VERSION ?? 'local';
const address = new URL(
`http://${process.env.AWS_LAMBDA_HTTP_ENDPOINT ?? '127.0.0.1:3000'}`
);
app.use('*', async (c, next) => {
const requestId = randomUUID();
const start = performance.now();
c.header('x-request-id', requestId);
c.header('cache-control', 'no-store');
await next();
console.info({
event: 'http_request',
appRequestId: requestId,
bootId,
build,
method: c.req.method,
status: c.res.status,
durationMs: Number((performance.now() - start).toFixed(3))
});
});
app.get('/health/live', (c) => c.json({ status: 'ok' }));
app.get('/v1/products/:id', (c) => {
if (c.req.param('id') !== 'sku-1') {
return c.json({ error: 'not_found' }, 404);
}
return c.json({ id: 'sku-1', name: 'Example product' });
});
if (process.env.ENABLE_LAB === 'true') {
app.get('/lab/wait', async (c) => {
await delay(100);
return c.json({ simulatedWaitMs: 100 });
});
}
app.onError((error, c) => {
console.error({ event: 'request_error', build, name: error.name });
return c.json({ error: 'internal_error' }, 500);
});
const server = serve({
fetch: app.fetch,
hostname: address.hostname,
port: Number(address.port)
});
console.info({ event: 'app_boot', bootId, build });
process.on('SIGTERM', () => server.close());This implementation uses Hono's server API and keeps request-specific data in request scope. bootId identifies a process for correlation; it is not a reliable detector of client-visible cold starts. (hono.dev)
The runtime loads index.js by default, and the server must bind to AWS_LAMBDA_HTTP_ENDPOINT. Do not replace that address with a hardcoded production port. (docs.aws.amazon.com)
The /lab/wait route models asynchronous waiting only. It does not simulate database connection setup, authentication, query execution or result serialization. Likewise, /health/live confirms basic HTTP responsiveness, not the health of every dependency.
ENABLE_LAB=true AWS_LAMBDA_HTTP_ENDPOINT=127.0.0.1:3000 npm startFrom another terminal:
curl --fail-with-body http://127.0.0.1:3000/health/live
curl --fail-with-body http://127.0.0.1:3000/v1/products/sku-1
curl --fail-with-body http://127.0.0.1:3000/lab/wait4. Deploy a lab endpoint
The following command uses documented deployment options and explicitly sets the runtime, timeout, concurrency and traffic limits. These values are lab choices, not universal recommendations. (docs.aws.amazon.com)
npm ci --omit=dev
aws lambda-web deploy \
--name hono-web-api \
--code . \
--create \
--runtime nodejs24.x \
--endpoint-name dev \
--endpoint-type HomeRegion \
--auth-type ApplicationManaged \
--auto-deployment-mode LatestRevision \
--timeout-seconds 10 \
--max-concurrency-per-environment 32 \
--max-environments 10 \
--rate-limit 100 \
--env ENABLE_LAB=true \
--env APP_VERSION=lab-v1
DOMAIN=$(aws lambda-web get-web-function-endpoint \
--function-name hono-web-api \
--endpoint-name dev \
--query domainName --output text)
export BASE_URL="https://${DOMAIN}"
curl --fail-with-body -i "${BASE_URL}/health/live"ApplicationManaged does not authenticate callers automatically. Since this sample implements no authentication, the lab is publicly accessible. Use an isolated account, synthetic data and a short testing window. Traffic limits do not replace access control. (docs.aws.amazon.com)
On first deployment, the CLI can create the code bucket and execution role. LatestRevision moves the development endpoint to new revisions. Retain the artifact and do not carry this deployment mode into production without considering its consequences. (docs.aws.amazon.com)
5. Execution, concurrency and cold starts
Three controls serve different purposes:
maxConcurrencyPerEnvironment: an upper bound on simultaneous requests per environment.maxEnvironments: an endpoint's environment limit in each Region.rateLimit: the incoming request rate limit.
Actual concurrency is dynamic and can remain below the configured ceiling. There is no minimum or reserved environment setting: the endpoint can scale back to zero. (docs.aws.amazon.com)
In this lab, 10 × 32 = 320 is only a theoretical regional concurrency ceiling, not guaranteed throughput. As an initial sizing exercise, 50 requests/s with an average duration of 0.2 s corresponds to roughly ten requests in flight. Validate that assumption under load, especially when response times have long tails.
An environment must start listening within ten seconds. Shutdown draining is best effort, and the process has up to one second for cleanup after SIGTERM. Do not rely on local memory or shutdown callbacks to preserve important work. (docs.aws.amazon.com)
For production, I propose small reusable connection pools. Ten environments with five connections each imply a potential fifty-connection budget for that endpoint and Region. Add other endpoints and clients before sizing the database.
6. Measure latency at the right boundary
Start with a single request:
curl --silent --show-error --output /dev/null \
--write-out 'status=%{http_code} dns=%{time_namelookup} connect=%{time_connect} tls=%{time_appconnect} ttfb=%{time_starttransfer} total=%{time_total}\n' \
"${BASE_URL}/v1/products/sku-1"curl's timings are cumulative milestones. time_starttransfer measures time to the first byte, while time_total covers completion. Do not add these values as though they were independent stages. (curl.se)
The middleware's durationMs covers a different interval: application processing until the response is prepared. It excludes the complete network path and does not measure when the client finishes receiving the body. It is not billable CPU time either.
Load testing with k6
Create load.js outside the deployment directory. The constant-arrival-rate executor starts iterations independently of response speed, provided enough virtual users are available. (grafana.com)
import http from 'k6/http';
import { check } from 'k6';
export const options = {
scenarios: {
api: {
executor: 'constant-arrival-rate',
rate: Number(__ENV.RATE || 50),
timeUnit: '1s',
duration: '2m',
preAllocatedVUs: 50,
maxVUs: 200
}
},
thresholds: {
http_req_failed: ['rate<0.01'],
http_req_duration: ['p(95)<250'],
checks: ['rate>0.99'],
dropped_iterations: ['count==0']
}
};
export default function () {
const path = __ENV.TEST_PATH || '/v1/products/sku-1';
const response = http.get(`${__ENV.BASE_URL}${path}`, {
timeout: '5s'
});
check(response, { 'HTTP 200': (r) => r.status === 200 });
}k6 run --summary-trend-stats='avg,med,p(95),p(99),max' \
-e BASE_URL="$BASE_URL" -e RATE=50 load.js
k6 run --summary-trend-stats='avg,med,p(95),p(99),max' \
-e BASE_URL="$BASE_URL" -e RATE=50 \
-e TEST_PATH=/lab/wait load.jsThese thresholds are hypothetical acceptance criteria, not promised performance. Failed thresholds fail the test; checks also need a threshold if they are to affect that outcome. (grafana.com)
I propose three separate campaigns:
- Initialization: request a revision with no previous traffic and correlate the response with initialization logs. Do not automatically label every first request a confirmed cold start.
- Steady state: warm up, measure in a separate window and repeat each scenario.
- Saturation: increase arrival rate gradually while watching errors, throttling and downstream systems.
Record revision, generator location, offered rate, completed rate, p50/p95/p99, failures and dropped iterations. Test real dependencies as well. AWS recommends distinct local, package, cloud integration, load and resilience test layers. (docs.aws.amazon.com)
7. Observability and resource interpretation
Use AWS/Lambda/Web metrics including ExecutedRequests, HTTPResponseStatusClass5XX, EnvironmentCPUTime, EnvironmentActiveTime and EnvironmentMaxMemoryUsage. CPU and active time use milliseconds; maximum memory is not an integral of billed consumption. Regional utilization and throttling metrics live in AWS/Lambda/Web/Usage. (docs.aws.amazon.com)
aws logs tail /aws/lambda/web/hono-web-api --since 10m --followLogs are centralized in the home Region, while .local groups retain the regional view. Configure retention for both. Logs might initially be absent before the first request or while centralization is taking place. (docs.aws.amazon.com)
For operations, I propose dashboards covering external latency, errors, consumption, throttling and synthetic availability. Define a service-level objective, or SLO, before automating promotion. Add dependency tracing when you need to distinguish database waiting, external calls and local execution; basic application logs should not be mistaken for complete distributed tracing. (docs.aws.amazon.com)
Do not calculate per-request CPU by taking process.cpuUsage() differences around an await under concurrency. The API measures process CPU, so the interval can include work from other requests. Use window-level aggregates and environment metrics to study efficiency instead. (nodejs.org)
8. Cost and FinOps: elapsed time is not active CPU
Billing includes requests, active CPU, memory consumed throughout environment lifetimes and data transfer. Waiting for I/O therefore does not mean zero cost: memory still accrues. The pricing page reviewed lists Northern Virginia/Arm rates of US$0.30 per million requests, US$0.0000276944 per vCPU-second and US$0.0000036667 per memory GB-second. Verify regional rates and terms before budgeting for production. (aws.amazon.com)
A cost model is:
Cost = requests × request unit price
+ active CPU in vCPU-seconds × CPU unit price
+ environment memory integral in GB-seconds × memory unit price
+ networking + observability + supporting servicesPurely hypothetical example: one million requests, 5,000 aggregate vCPU-seconds and ten environments consuming 0.25 GB for four hours each.
Memory = 10 × 0.25 × 14,400 = 36,000 GB-seconds
Requests = US$0.30
CPU = 5,000 × 0.0000276944 = US$0.138472
Memory = 36,000 × 0.0000036667 = US$0.1320012
Illustrative subtotal ≈ US$0.57This subtotal excludes transfer, logs, CloudFront, WAF, databases, storage and any discounts. It is not a complete API budget. The example assumes aggregate resource consumption rather than multiplying memory by the duration of every concurrent request.
My proposed FinOps measure is cost per million successful responses, with explicit traffic and retention controls and reconciliation against billing. Do not automatically reuse the allocated-memory-times-duration formula from Event Functions. (aws.amazon.com)
9. Moving beyond the lab
| Area | This lab | Proposed production baseline |
|---|---|---|
| Access | Public, synthetic data | IAM or validated application authentication |
| Deployment | LatestRevision | Approved revisions and explicit weights |
| Testing | Static catalog and simulated waiting | Real dependencies, failures and load |
| Operations | Basic logs | SLOs, alerts, traces and runbooks |
| State | No persistent data | External persistence and tested recovery |
| Changes | Immediate updates | Canary rollout and rollback criteria |
Before promotion, disable ENABLE_LAB, remove sensitive material from the package and separate deployment, execution and invocation identities. Grant the execution role only the access it needs, and keep secrets out of source code and logs. (docs.aws.amazon.com)
For CloudFront, use origin access control, OAC, with IamAuth and a policy scoped to the distribution. Origin authorization does not replace end-user authorization. Regional IAM requests use SigV4, while global MultiRegion requests use SigV4a. (docs.aws.amazon.com)
Availability and networking constraints
MultiRegion routes by latency; it does not provide health-based failover. PerRegion exposes regional domains for externally managed routing. Neither option automatically solves data replication, database recovery or write consistency. (docs.aws.amazon.com)
In the documentation reviewed, outbound VPC connectors are supported only with HomeRegion. Do not assume that a private PostgreSQL connection through a connector can be combined with a MultiRegion endpoint. Include this restriction in the architecture decision. (docs.aws.amazon.com)
My recommendation is to start near the data, define recovery objectives, exercise restoration and adopt multiple Regions only with an explicit dependency and failure strategy.
10. Revisions, canaries and rollback
First deploy a revision with ENABLE_LAB=false and a meaningful APP_VERSION. Validate it through the test endpoint and retrieve its identifier:
aws lambda-web list-web-function-revisions --function-name hono-web-apiReplace rev-STABLE with a real active revision. This command creates an authenticated production endpoint without automatic promotion. (docs.aws.amazon.com)
aws lambda-web create-web-function-endpoint \
--function-name hono-web-api \
--endpoint-name prod \
--endpoint-type HomeRegion \
--auth-type IamAuth \
--auto-deployment-mode Disabled \
--revision-weights '[{"revisionId":"rev-STABLE","weight":100}]' \
--scaling-config '{"maxEnvironments":10}' \
--throttle-config '{"rateLimit":100}'After publishing and testing another revision, route, for example, 90% to the stable revision and 10% to the candidate. Weights must total 100. To roll back, include only the stable revision at weight 100; do not assign a zero weight. (docs.aws.amazon.com)
aws lambda-web update-web-function-endpoint \
--function-name hono-web-api \
--endpoint-name prod \
--auto-deployment-mode Disabled \
--revision-weights '[{"revisionId":"rev-STABLE","weight":100}]'
aws lambda-web wait web-function-endpoint-updated \
--function-name hono-web-api --endpoint-name prodPreserve artifacts and S3 object versions needed for recovery. I recommend database migrations compatible with both revisions. Code rollback does not reverse data changes or runtime patches: Web Functions automatically updates the runtime and does not support patch-version pinning. (docs.aws.amazon.com)
11. Evidence-driven troubleshooting
Start with these commands:
aws lambda-web get-web-function-endpoint \
--function-name hono-web-api --endpoint-name dev
aws lambda-web get-web-function-revision \
--function-name hono-web-api --revision-id rev-REPLACE
curl -i "${BASE_URL}/health/live"
aws logs tail /aws/lambda/web/hono-web-api.local --since 10mReplace rev-REPLACE with an existing revision identifier.
| Symptom | Possible cause | Investigation | Correction |
|---|---|---|---|
| Active, but HTTP fails | Entry point or startup problem | Logs and listening address | Fix the package and publish a revision |
| HTTP 429 | Regional or endpoint limit | lambda-web-error-type and metrics | Adjust load, capacity or quota |
| HTTP 403 | Signing or IAM problem | AuthType, policies and signature | Fix authorization rather than opening access |
| p99 rises under load | Event-loop or dependency saturation | CPU, traces and pools | Remove blocking work and control concurrency |
The lambda-web-error-type header helps distinguish platform failures from application responses. Investigate before enabling indiscriminate retries. (docs.aws.amazon.com)
12. Where it fits, alternatives and common mistakes
I recommend evaluating Web Functions for Node.js HTTP APIs, applications with substantial I/O waiting and teams that want a web-server programming model without managing execution infrastructure. Consider Event Functions for queues and event-driven processing, and another execution platform when requirements fall outside this model. (docs.aws.amazon.com)
As an architecture decision, also compare keeping the service on a container platform the organization already operates. Include maintenance, network controls, authorization, operations and total cost—not just CPU. This lab does not depend on Kubernetes and requires no Pod manifests, probes or resource requests.
Avoid promoting the public lab endpoint, keeping user-specific state in globals, blocking the event loop with heavy synchronous work, treating configured ceilings as guaranteed capacity or equating regional replication with complete disaster recovery. (docs.aws.amazon.com)
When the lab is finished:
aws lambda-web delete-web-function --function-name hono-web-apiDeletion removes revisions and endpoints, but does not automatically clean up the role, S3 objects and versions, or log groups. Review those resources separately without deleting shared infrastructure. (docs.aws.amazon.com)
Practical summary and conclusion
The proposed workflow is straightforward: deploy a minimal API, verify HTTP behavior, measure initialization separately from steady state, inspect aggregate CPU and memory, calculate the complete cost and rehearse rollback before production.
For DevOps and Platform Engineering, this can become a standardized delivery path with reproducible artifacts, mandatory authentication and controlled promotion. For SRE, success still depends on evidence: meeting the service objective, protecting dependencies and recovering predictably. Serverless changes the operational responsibilities; it does not remove the engineering work.
Related articles
Want to apply this to your environment?
Book a free 30-minute call. I will look at your setup and tell you where to start.
Book a callGet new articles by email
One email when a new article on infrastructure, backend and AI is published. No spam, unsubscribe anytime.