Web Functions
Lambda Web Functions run a Node.js HTTP server that listens on a port. One execution environment serves many requests at once, instead of one request per environment as with the Event Functions that the rest of this site measures. This page collects what a set of one-off probes measured about their cold starts, warm latency, concurrency and cost, and how they compare with Event Functions running the same handlers.
Web Functions are not part of the benchmark matrix: they support only Node.js and have a fixed environment size, so most of the matrix's dimensions do not apply. This is a dated investigation from 2026-10, shortly after launch; Lambda's scaling behavior in particular may have changed since.
| Question | Measured answer |
|---|---|
| Cold start | Slower than an Event Function: hello ~380 ms vs ~225-240 ms caller wait. Most of the extra time passes before the app's code runs |
| Warm request | Faster: ~4 ms vs ~11-14 ms caller wait |
| Cold starts per burst | Far fewer once the endpoint has learned the handler: 1,000 simultaneous requests started 16 environments |
| A new endpoint | Starts at ~2 requests per environment and learns over minutes of traffic |
| CPU-bound handlers | One Node event loop per environment; at the default concurrency, requests queue behind it |
| Idle environments | Reclaimed ~5.5-7 min after their last request, and billed for memory until then |
| Background work | Timers, threads and child processes keep running between requests, billed as CPU, until the environment is reclaimed |
| Slow startup | Startup gets ~9-10 s, and a failed one is not retried. Requests wait for a starting environment, and that wait does not count against the timeout; work deferred past listen() does |
| vs a Rust Event Function | smithy: Rust cold ~160-175 ms vs ~460-480 ms; Node Web warm ~4 ms vs ~13 ms |
Rust via execve |
oneclient: ~390 ms cold from the entry module, ~314 ms from a preload, vs ~441 ms for Node and ~220 ms for a Rust Event Function |
One-off probes taken over several days in 2026-10 in a single account in eu-central-1, runtime
nodejs24.x (Web Functions runtime version nodejs.web24.v14). They are not committed benchmark
data, so treat them as rough magnitudes; where a repeat run differed, the tables give both.
The handlers are this benchmark's Node hello, oneclient and smithy bundles, unchanged. For
the Web Function each was imported by a minimal node:http server (no web framework) that calls
the handler per request, and the Event Function baselines ran the same bundles as normal handlers
(arm64, 2,048 MB); the Rust comparison used this benchmark's rust-smithy-arm64-o3.zip. A Lambda
function in the same region timed every request as the time to the first response byte on an
already open TLS connection, which leaves out the TLS handshake to Lambda's endpoints (~27 ms
in-region). A new Web Function revision is live within 5-10 s and starts fresh environments, which
is how the Web cold starts were forced; Event Functions were forced cold by a configuration update,
and a per-process request counter confirmed each cold sample. Cold cells have 6-15 forced cold
starts each, warm rows 600-900 sequential requests.
Inside the environment
These points come from inspecting a running environment, except where marked documented, and can change.
The environment is fixed at 2 vCPUs and 2 GB on arm64 (documented). As with Event Functions, the
host CPU varies per environment: in three bursts, 49 of 60 environments ran on Graviton4 and 11 on
Graviton3, none on Graviton2, so the cold-start numbers here mix generations (see reading the cold
numbers). The kernel identifies itself as a microVM build
(…aarch64.microvm). The process list visible to the server shows only the sandbox's init process
and one node process running AWS's wrapper /var/runtime/index.js. The wrapper imports the entry
file (index.js by default, or the file named in AWS_LAMBDA_NODEJS_ENTRYPOINT), then tries every
5 ms to connect to the address in AWS_LAMBDA_HTTP_ENDPOINT (0.0.0.0:3000), and reports the
environment ready once a connection succeeds; the documented limit for listening is 10 s, but in
practice the wrapper allowed 9 s (see slow startup).
Requests do not reach the server directly. Lambda terminates TLS and forwards plain HTTP/1.1, and
the server sees the connection coming from 169.254.100.1, not from the client. That link-local
address is also the one in AWS_LAMBDA_RUNTIME_API; which Lambda component holds it, and whether
that component runs inside the microVM, is not visible from the server.
Each environment writes platform.initReport (init duration, CPU and memory),
platform.invokeSummary about every 60 s, and platform.invokeReport at shutdown to the
function's log group, which together give each environment's CPU time, lifetime and peak memory.
Cold starts
| Handler | Web Function caller wait, p50 (p10-p90) | Event Function Invoke caller wait, p50 (p10-p90) |
|---|---|---|
hello |
~378 ms (321-422); repeat: ~382 ms (314-435) | ~225 ms (205-254); repeat: ~240 ms (225-396) |
oneclient |
~497 ms (405-557); repeat: ~456 ms (432-513) | ~424 ms (339-533); repeat: ~426 ms (420-457) |
smithy |
~457 ms (352-527); repeat: ~460 ms (367-493) | ~371 ms (304-408); repeat: ~404 ms (362-431) |
A Web Function cold start took ~30 to ~155 ms longer. The gap was largest for hello, whose own
init is the smallest, and smaller for the two handlers with heavier init.
Where the time goes
The server read its own timestamps relative to the start of the node process
(performance.timeOrigin). For hello:
| Step | Time since the Node process started |
|---|---|
| The entry module's first line runs | ~141-144 ms |
listen() done |
~154-158 ms |
| First request arrives (readiness check and routing) | ~180-185 ms |
An ES module's first line runs only after its static imports have loaded, here node:http,
node:crypto, node:fs and node:os. From there to listening, which covers importing the hello
handler and starting the server, took ~13 ms. The ~141 ms before it went to Node's startup, the
wrapper and those imports, while the same bundle as an Event Function completes its whole Init in
~97-103 ms.
A separate probe split that time up in five bursts of 20 environments each, the last three of which
also recorded the host CPU. Its entry module imported only node:http and node:child_process, and
a preload (NODE_OPTIONS=--import) recorded each module the wrapper loaded, its calls to the
runtime API, and CPU time along the way. Node finished bootstrapping at ~30 ms. Loading the
wrapper's file took under 1 ms, and its built-in imports up to http ~7-9 ms. Then importing http
as an ES module read every export, including the lazy WebSocket getter, and that getter loading
Node's bundled undici took ~43-61 ms, the largest single step. The rest of the wrapper's own work,
from there to the entry's first line, took ~12-15 ms; its first call to the runtime API, which
fetches the function's configuration, took ~1.5 ms of that. The entry's first line ran at ~109-154
ms. CPU time grew as fast as wall time throughout, so this startup was compute-bound, not waiting on
disk or network. The host CPU set the pace: on Graviton4 (49 of the 60 environments) undici took
~43.5 ms and the first line ran at ~113 ms, on Graviton3 (11) ~58.5 ms and ~150 ms, with every step
about a third longer.
require("http") from CommonJS does not touch the getter. Node tracks the ES module case as
nodejs/node#59686, closed with a documentation
change on how import and require load built-in modules. The same undici also backs the global
fetch(), so an app that calls it pays this once anyway.
An app that never loads undici would gain less than this step costs, because typical app code
loads most of undici's dependencies itself. In a simulation (a preload that keeps the wrapper's
import from loading undici), an Express app reading one DynamoDB item finished its first GetItem
~16 ms earlier on Graviton4 (48 fix vs 38 control environments) and ~23 ms earlier on Graviton3
(10 vs 22), and the fix variant never loaded undici.
Of the ~380 ms caller wait, ~200-215 ms lies outside the Node process's lifetime (the caller wait minus the process's age when it responded). That remainder presumably is mostly environment setup and the code download, plus the request's way through Lambda, but it cannot be split further from inside.
A web framework adds its own startup before listen(). These probes used none, so they measured
neither that cost nor whether deferring setup until after listen() shortens the first request.
Platform-reported init is not comparable to Init Duration
| Handler | Web Function initReport, p50 |
Event Function Init Duration, p50 |
|---|---|---|
hello |
~265 ms | ~97-103 ms |
oneclient |
~330 ms | ~237 ms |
smithy |
~340 ms | ~231-254 ms |
The Web Function's init clock includes time that grows with the package size, most likely the code
download. Adding a 50 MB incompressible file to the hello package raised initReport by
~145-150 ms and the caller wait by ~150-185 ms, in line with the
download cost of a .zip, while an Event Function's
Init Duration starts after the download. The two columns therefore overstate the gap, and the
caller waits above are the fair comparison. initReport recorded ~190 ms of CPU for an empty
hello; the EnvironmentCPUTime metric includes init (documented), and Web Functions bill CPU
time, but no bill was checked against these numbers.
Slow startup
A probe server delayed its own startup in two ways: with a top-level await in the entry module,
so the wrapper's import of it did not finish, and with a timer before listen(), after the import
had finished. Each case was a new revision of the hello server, with sequential requests from the
in-region client (2026-10, a few requests per case).
Startup limits
| Limit | Cutoff | Caller gets |
|---|---|---|
| Entry module import | 9 s (customer code did not finish loading within 9000ms) |
HTTP 502, Runtime.TimeoutImportError |
listen(), counted from the import's end |
9 s (nothing was listening on 0.0.0.0:3000 within 9000ms) |
HTTP 502, Application.TimeoutListenError |
Whole init (initReport) |
~10 s (failed at 10,060-10,092 ms) | HTTP 502, Runtime.InitTimeout |
An 8 s import, an 8.5 s delay before listen(), and a 5 s import followed by listen() 4.5 s
later (initReport 9.7 s) all succeeded. Two 8.5 s steps, whose sum is over 17 s, failed at the
10 s limit. The documentation names a 10 s limit for listening and does not list
Runtime.TimeoutImportError. So startup work at the top level of the entry module gets 9 s, and
all of startup gets about 10 s on the platform's clock, which also counts time before the Node
process starts, most likely the code download (see
platform-reported init).
A failed startup is not retried
Each request that met a too-slow startup waited ~9.3-10.2 s and got a 502. Lambda shut that
environment down, and the next request started a new one, which failed the same way. Unlike an
Event Function, whose init over 10 s
runs a second time inside the first invoke,
a Web Function never ran a second startup for the same request, so a slow startup is not paid twice,
but no request succeeds until a fixed revision is deployed. Each failed startup still used CPU, which
Web Functions bill: ~190 ms when the import was held, and ~365 ms when only listen() was late,
probably because the wrapper tries to connect every 5 ms the whole time. After a Runtime.InitTimeout the
process was not stopped at once: it kept running, called listen() ~7 s later and then failed to
report readiness to the runtime API.
Waiting for startup is not part of the request timeout
With timeoutSeconds at 3 (its minimum) and listen() 8 s after the import, the first request
waited 8.4 s and succeeded. The timeout started once the request reached the server.
A crash replaces the environment
A request that ended the process (process.exit(1)) got HTTP 503 with
Runtime.OriginRequestError within ~40 ms. Lambda shut that environment down instead of starting
the server again in it, and the next request started a new environment with the full startup budget
(8 s startup: 8.4 s wait, then 200). An Event Function's
re-init loop, where every
invoke re-runs a startup that cannot fit in its timeout, did not occur.
Requests queue on a starting environment
With a 5 s startup and 1 s requests, every burst below started from zero environments, after Lambda had reclaimed all of them, so no warm or spare environment could take a request:
| Burst | Result |
|---|---|
| Learned endpoint, 64 simultaneous requests | All 64 on 1 environment, each waited ~6.5 s (startup + request) |
| Learned endpoint, 1 request, then 63 more 2 s into its startup | All 64 on that 1 environment; the 63 waited ~4.4 s |
| Learned endpoint, 128 simultaneous requests | 2 environments × 64, each request ~6.5 s |
| New endpoint, 64 simultaneous requests | 32 environments × 2, each request ~6.4 s |
Requests that arrive while an environment starts wait for it, up to the endpoint's per-environment limit, and are not throttled (documented). They also stayed with the environment they were queued on. Lambda also started spare environments, in the simultaneous bursts one that finished starting within ~50 ms of the busy one, and none of them served a queued request. On a learned endpoint, then, a slow startup delays every request in a burst of up to the limit, not only the first, and a burst does not spread out to finish sooner. In an earlier run, a new endpoint added the same way ran its first burst of 64 on one environment instead of 32 × 2; that run is not explained.
Deferring startup work past listen()
AWS recommends listening first and letting requests await the remaining startup work. A server that
listened at once and made every request wait 12 s for a startup promise, longer than any limit
above, worked. A burst of 64 from zero ran on one environment, and every request succeeded after
~13.4 s. The wait then counts against timeoutSeconds instead of the ~9-10 s startup limit,
unlike the wait before listen(): with timeoutSeconds at 5, all 64 requests got HTTP 504
(Runtime.RequestTimeout) after ~5.5 s.
Warm requests
Warm hello, 900 sequential requests per row (3 interleaved rounds):
| Path | p50 | p90 | p99 |
|---|---|---|---|
| Web Function, no authentication | ~4.1 ms (repeat: 3.8) | ~4.4 ms | ~5.4 ms |
| Web Function, IAM authentication (SigV4) | ~3.6 ms (repeat: 3.7) | ~4.1 ms | ~4.7 ms |
Event Function, Function URL, auth NONE |
~11.3 ms (repeat: 12.4) | ~14.7 ms | ~20.3 ms |
Event Function, Invoke without log tail |
~10.8 ms (repeat: 11.5) | ~13.6 ms | ~18.9 ms |
Event Function, Invoke with LogType=Tail |
~14.0 ms (repeat: 13.5) | ~17.6 ms | ~22.7 ms |
IAM authentication does not explain the gap: signed Web Function requests were no slower than
unsigned ones. The Event Function's extra ~7-8 ms appears even through an unauthenticated Function
URL, and it is consistent with the per-invoke front-end lump that the
production-caller probe measures for
every runtime. A Web Function request also passes through Lambda (it reaches the server from
169.254.100.1, see inside the environment), so it is not a direct
connection. A same-region request that skips Lambda entirely, from a Lambda function to Fargate
behind an Application Load Balancer, took
~0.7 ms at p50 and ~1 ms at p99 (a different
client: Rust over HTTP/2), so a Web Function request still pays ~3 ms of Lambda's own per-request
cost, less than an invoke's but not zero. What it saves internally is not visible from outside.
Seeing the caller wait from outside
Lambda publishes no latency metric for Web Functions (its documented metrics count requests and
environment resources), and neither timing inside the server nor initReport covers the time before
the Node process exists. The cold-start numbers on this page therefore come from client-side timing.
CloudFront in front of the endpoint sees that time too: in two separate runs with caching off and
standard logs on, its time-to-first-byte matched the in-region client's wait within ~2 ms on all
20 forced cold starts (~330-495 ms). The edge was in the client's region; a remote user would add
their own network time on top.
CloudFront added ~1-2.5 ms at warm p50 over a direct request (~4.5-6.4 vs ~3.6-4.0 ms), and its p99
rose from ~6-9 to ~48-51 ms. In both runs, a few requests right after each cold start took 20-40 ms
on an already warm environment, probably while CloudFront opened new connections to the origin. Its
origin-fbl field usually ran a few ms below time-to-first-byte but ~30-60 ms below on some cold
starts, so it is no substitute for time-to-first-byte.
Concurrency
Concurrency is learned per endpoint
maxConcurrencyPerEnvironment (default 64, 1-128) is only an upper bound. Lambda sets each
environment's actual limit from the handler's observed CPU and memory use (documented), and in these
probes the limit started low and was learned per endpoint:
| Setup, I/O-bound requests (1 s wait each) | Result |
|---|---|
| New function, 64 simultaneous requests | 32 environments × 2 requests (both runs) |
| Same endpoint after a few minutes of traffic, from zero environments | 1 environment × 64 |
| A new endpoint on that same function, 64 requests | 32 × 2 again (both runs) |
| Learned endpoint after 33 min with no environments, 64 requests | 1 new environment × 64 |
Learned endpoint, maxConcurrencyPerEnvironment 128, 256 requests |
3 environments, ~90-110 requests on the busiest |
| Learned endpoint, 1,000 simultaneous requests | 16 environments, 64 requests on all but one; all succeeded |
With 40 clients looping on 200 ms requests against a new function, the endpoint first ran 29 environments at ~2 requests each, then 4 at ~10 after ~45 s, and 2 at ~20 from ~100 s on.
The learned limit survived new revisions, including code changes, and was still in place after 33 minutes at zero environments, the longest gap tested; hours or days without traffic were not tried. On a second function, learned the same way, a burst of 64 ran on 1 environment after a comment-only code change, and again after 15 minutes idle and a change that made each request burn 100 ms of CPU. In that last burst the 64 requests took p50 4.8 s and up to 7.9 s, against ~1.5 s before: a release that makes the handler heavier starts at the limit learned from the old code. Until an endpoint has learned, a burst starts about one environment per two requests (32 for 64), much closer to an Event Function's one per request than to the learned 64.
Each environment counts 2 vCPUs against a regional Arm vCPU quota (200 vCPUs, so 100 environments,
in this account). Above it, requests failed with HTTP 429 and
Platform.AccountActiveVCPUArm64Throttle.
Spare environments
Lambda starts environments ahead of demand (documented), and some never receive a request. At the
default maxConcurrencyPerEnvironment of 64, ten simultaneous requests to a new function started 8
environments: 5 served 2 requests each and 3 stayed unused. On a learned endpoint, bursts of 256 and
512 requests ran on 4 and 8 environments at 64 each and started 2 and 4 more that stayed unused. A
spare environment lives as long as any idle one (see scale-down) and uses init CPU
and memory like the rest.
Under steady traffic few requests met a cold start. With 800 concurrent clients on a learned
endpoint, 18 environments carried ~45 requests each, and of 701,510 requests only those in the
first burst from zero waited on a cold start. Of 397,000 oneclient requests from 20 clients,
20 (0.005%) took more than 250 ms longer than usual, and all 20 were in the opening burst, waiting
on the 15 environments it started.
CPU-bound handlers
Node runs JavaScript on one thread, so a busy event loop blocks every request in its environment. With 50 ms of synchronous CPU per request and 40 looping clients:
maxConcurrencyPerEnvironment |
Environments | Throughput | Latency p50 |
|---|---|---|---|
| 64 (default) | 1-2 at first, then about one more per minute, 4 after ~3 min | 20-40, rising to ~80 req/s | ~1,000-1,400 ms, ~300 ms once 3-4 environments ran |
| 1 | 58 from the start (both runs) | ~720-730 req/s | ~55 ms |
At the default, requests queued behind the event loop while Lambda added environments slowly, and
lowering maxConcurrencyPerEnvironment removed the queueing. Moving the work to worker_threads
could use the environment's second vCPU, but was not tested.
Scale-down
Idle environments were reclaimed mostly 330-420 s after their last request, across several hundred environments and including spare ones that never served a request (those lived ~350-400 s from init); a few went sooner. Under continuing traffic, surplus environments went away within ~30 s.
A revision switch is not instantaneous. For a few seconds after a new revision went live, warm
environments of the previous revision still served requests, and in one burst 257 distinct
environments served requests, more than the account's quota of 100. The documentation mentions a
temporary overshoot of maxEnvironments during revision changes; whether the account quota is
treated the same way is not documented.
Before removing an environment, Lambda stops routing to it, lets in-flight requests finish and sends
SIGTERM, after which the process has up to 1 s (documented). Every one of ~190 environments that
spun down during the repeat run received the signal, and the log line its handler wrote reached
CloudWatch; spin-down took under 90 ms. AWS does not guarantee the signal in every failure, so it
suits flushing buffered data, not work whose loss would break correctness.
Background work between requests
An Event Function's environment is frozen between invokes, so timers and threads stop until the next request (documented). A Web Function environment was not. A probe started background work with one request and then sent nothing until the environment was reclaimed, ~10 minutes later:
| Background work | What happened while no requests arrived |
|---|---|
setInterval on the main thread (1 s) |
Fired every second until SIGTERM (600 of 600 ticks); no gap longer than ~1 s |
| A worker thread (1 s timer, optionally a busy loop) | Kept running; the busy loop held a steady ~18 million iterations per second |
| A child process | Kept running until shutdown |
A setTimeout scheduled after the response ended |
Fired on time (5 s) |
| An outbound HTTPS request every 10 s | All succeeded (~65 ms each) |
console.log every 30 s |
Reached CloudWatch |
The process's monotonic clock advanced by 600.8 s between the start request and SIGTERM,
matching the ~601 s of real time between them, and no timer gap exceeded ~1 s, so the environment
was not paused. Background work shows up in the environment's CPU time, which Web Functions
bill: with only the timers and the network calls, an idle environment used ~190-200 ms of CPU per
minute instead of ~95 ms, and with a worker thread spinning, ~60 s per minute, a full vCPU for as
long as the environment lived. The background activity did not keep the environment alive,
though: both were reclaimed 358 s and 400 s after their last request, within the usual range, so
anything running in the background ends with the environment, at the latest after the 1 s that
follows SIGTERM.
Node Web Function vs Rust Event Function
Both running the benchmark's smithy handler (cold: 10-15 forced cold starts per cell; warm: 600
requests each):
| Configuration | Cold caller wait, p50 (p10-p90) | Warm caller wait, p50 / p99 |
|---|---|---|
| Node Web Function (2 vCPU, fixed) | ~478 ms (390-517); repeat: ~460 ms | ~3.9 / ~5.6 ms |
| Rust Event Function, 2,048 MB, Function URL | ~161 ms (149-211); repeat: ~174 ms | ~13.3 / ~24.5 ms |
| Rust Event Function, 128 MB, Function URL | ~152 ms (131-219); repeat: ~170 ms | ~12.0 / ~25.6 ms |
Rust Event Function, 2,048 MB, Invoke |
~155 ms (138-203) | ~14.1 / ~23.8 ms |
Rust smithy's own Init is ~33 ms, so nearly all of its cold wait is Lambda's setup, and memory
size barely changed it. The Rust Event Function's cold starts were about three times faster, while
the Node Web Function was ~9 ms faster warm at p50 and ~19 ms at p99, consistent with the invoke
path's per-request cost applying to Rust as much as to any runtime.
A Web Function has 2 vCPUs before and after it reports ready. An Event Function's handler gets a CPU
share that scales with its memory size, and the benchmark's own measurements show what that costs
when the first request runs code that has not been loaded yet: the page faults decompress the
package on the CPU, so at small sizes they stretch. smithyfull on arm64 took ~382 ms cold at 128
MB and ~175 ms at 1,024 MB (why the first request is
slow). Against small Event Functions with
code-heavy first requests, the fixed 2 vCPUs are therefore a cold-start advantage; against 1,024 MB
and up they make no difference, and the smaller smithy above showed none even at 128 MB. Reading
the executable at init, which helps small Event Functions because their Init gets more CPU than
their handler, has nothing to gain on a Web Function. Whether a Web Function's code sits on the same
compressed image was not checked.
Which is faster on average depends on the share of requests that meet a cold start. Taking each side's p50 as typical and assuming the Rust function stays warm, the Web Function has the lower mean latency unless more than about 2% of its requests are cold. With traffic that rarely stops nearly every request is warm, and there the Node Web Function was faster; with bursts after long silences most first requests are cold, and there the Rust Event Function was faster, ~160-175 ms against ~460-480 ms. At list prices the Rust function would also cost less per request ($0.20 per million plus a small duration charge, against $0.30 per million plus CPU and memory), but its cost was not measured.
Rust on a Web Function via execve
The runtime is Node.js only, but the Node process can replace itself with another server through
process.execve, as shown for
Go. The server
then listens on AWS_LAMBDA_HTTP_ENDPOINT and reports readiness to the runtime API itself. A probe
ran an axum server with the benchmark's oneclient handler (one DynamoDB client built at startup,
one GetItem per request) in two ways: from the entry module, after AWS's wrapper ran, and from a
NODE_OPTIONS=--require preload, before it. In the preload variant the Rust server also fetched the
function's configuration from the runtime API (/load) as the wrapper would have, and Lambda
accepted its readiness report in all 10 cycles. Neither way is documented, and the preload depends
on runtime details that can change. All rows ran in the same hour, 9-10 confirmed cold starts each:
oneclient |
Cold caller wait, p50 (p10-p90) | Graviton4 only | First request after process start | Warm caller wait, p50 |
|---|---|---|---|---|
| Node Web Function | ~441 ms (424-569) | ~433 ms (n=8) | ~190 ms | ~7.5 ms |
Rust Web Function, execve from entry module |
~390 ms (345-460) | ~365 ms (n=6) | ~136 ms | ~5.1 ms |
Rust Web Function, execve from preload |
~314 ms (289-369) | ~308 ms (n=9) | ~70 ms | ~5.4 ms |
| Rust Event Function, 2,048 MB, Function URL | ~220 ms (206-353) | not recorded | ~15.9 ms | |
Rust Event Function, 2,048 MB, Invoke |
~227 ms (201-252) | not recorded |
From the entry module, execve happened at ~106 ms on Graviton4, after Node's startup and the
wrapper, so Rust saved only the JavaScript handler's own startup. From the preload it happened at
~34 ms, and the first request reached the Rust server at ~70 ms, close to the Event Function's
Init Duration (~69 ms p50 here; ~57 ms on Graviton4 and ~71 ms on Graviton3 in the 2026-10
benchmark run). Every Web Function variant still spent ~200-220 ms before its process existed, so
even the fastest one was ~90 ms slower cold than the Rust Event Function (~314 vs ~220 ms),
though faster warm (~5.4 vs ~15.9 ms). Replacing the Node process also drops what the wrapper adds:
request IDs in logs and its SIGTERM handling become the server's job.
Cost
Web Functions bill requests, active CPU time and the memory each environment uses for its whole
lifetime, idle time included (pricing). In eu-central-1
at the time of the probes that was $0.30 per million requests, $0.0000349964 per active vCPU-second
and $0.0000046334 per GB-second; Event Functions on arm64 cost $0.20 per million requests plus
$0.0000133334 per GB-second of configured memory for the invocation's duration.
| Measured | Value |
|---|---|
CPU per request, hello / oneclient |
~0.8 ms / ~1.6 ms, including the server (both runs) |
| CPU of an idle environment | ~60-120 ms per minute |
| Environment memory, idle / under load | ~145-155 MB / ~250-440 MB (peak memory, so an upper bound) |
| One environment that served a single request | ~6 min alive, ~0.8 s CPU; ~$0.0003 at list prices |
The following are list-price estimates from that consumption, not checked against a bill, and they take peak memory for the memory Lambda meters. A cold environment costs much more than an Event Function cold start, mostly for its idle memory until it is reclaimed: one request every 10 minutes would cost about $1.25 a month, where the same traffic on an Event Function costs a fraction of a cent. Short handlers cost more per request, because the request price ($0.30 against $0.20 per million) is most of their cost. Requests that mostly wait cost less, because waiting uses no CPU and concurrent requests share one environment's memory: a request that waits 200 ms with ~1.5 ms of CPU works out to about $0.38 per million, against ~$1.53 for a 512 MB Event Function.