Back to blog

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

  1. Web Function: the application resource.
  2. Revision: an immutable snapshot of code and configuration.
  3. 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-server

Commit 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 start

From 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/wait

4. 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.js

These 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:

  1. 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.
  2. Steady state: warm up, measure in a separate window and repeat each scenario.
  3. 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 --follow

Logs 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 services

Purely 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.57

This 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

AreaThis labProposed production baseline
AccessPublic, synthetic dataIAM or validated application authentication
DeploymentLatestRevisionApproved revisions and explicit weights
TestingStatic catalog and simulated waitingReal dependencies, failures and load
OperationsBasic logsSLOs, alerts, traces and runbooks
StateNo persistent dataExternal persistence and tested recovery
ChangesImmediate updatesCanary 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-api

Replace 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 prod

Preserve 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 10m

Replace rev-REPLACE with an existing revision identifier.

SymptomPossible causeInvestigationCorrection
Active, but HTTP failsEntry point or startup problemLogs and listening addressFix the package and publish a revision
HTTP 429Regional or endpoint limitlambda-web-error-type and metricsAdjust load, capacity or quota
HTTP 403Signing or IAM problemAuthType, policies and signatureFix authorization rather than opening access
p99 rises under loadEvent-loop or dependency saturationCPU, traces and poolsRemove 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-api

Deletion 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.

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 call

Get new articles by email

One email when a new article on infrastructure, backend and AI is published. No spam, unsubscribe anytime.