PasskeyBridge

Engineering · 2026-05-26

Cold Start Latency in Serverless Identity Functions: Benchmarks and Mitigations

By J. W. Bouckaert

Cold Start Latency in Serverless Identity Functions: Benchmarks and Mitigations

Cold starts and identity workloads

Most serverless workloads tolerate a cold start. A user clicking a marketing CTA does not feel 300 ms. A nightly ETL does not care about 800 ms. Identity verification cares about every one of those milliseconds because the verification step is gating something the user is actively waiting on: a sign-in, a transaction, a step-up challenge during a checkout. The P99 tail on the verification function is the user-visible tail on the whole product.

This is also the workload where the in-process work is genuinely expensive. A modern identity verifier has to load a crypto library, parse key material, instantiate a WASM-compiled post-quantum module, open a database connection, fetch a tenant configuration, and—in a hybrid signature stack—do all of that twice. None of it is gratuitous, and all of it has to complete before the first signature can be verified.

A 5 ms platform boot time tells you almost nothing about a real identity workload. The number that matters is time-to-first-verification on a runtime instance that did not exist before the request arrived.

The rest of this piece is the engineering picture. We characterise three serverless runtimes—AWS Lambda, Cloudflare Workers, and Supabase Edge Functions (Deno Deploy)—on a representative identity verification path, then break down the four initialization strategies that compress the cold-start budget enough to be acceptable in production.

The reference workload

The reference workload is intentionally close to a real PasskeyBridge verification: receive a request containing a hybrid ES256 + ML-DSA-65 signature, verify both signatures, look up a tenant key from Postgres, and return a decision. Three variants of the workload are worth measuring on each runtime:

  1. Pure platform boot. A handler that returns "ok" with no imports beyond the runtime's own globals.
  2. Classical-only verification. Adds ES256 (ECDSA P-256) verification using the runtime's native crypto, plus a pooled Postgres lookup.
  3. Hybrid verification. Adds ML-DSA-65 verification via a 480 KB WASM module compiled with wasm-pack from the Rust ml-dsa crate.

The numbers below are representative ranges drawn from many runs. They are drawn from each platform's own published cold-start documentation, AWS Lambda's init-duration disclosures, Cloudflare's V8-isolate cold-start claims, the Deno Deploy isolate model, and the well-known cold-start studies that have repeatedly characterised these runtimes. Treat them as the shape of the curve rather than a single-decimal verdict; the relative ordering is stable across measurement methodologies, the absolute numbers move with platform updates.

Cold-start latency: Representative ranges

WorkloadAWS Lambda (Node 20, 1024 MB)Cloudflare WorkersSupabase Edge Functions (Deno)
Platform boot only—P50~120–180 ms~5 ms~30–50 ms
Platform boot only—P99~250–350 ms~10 ms~80–120 ms
Classical verification—P50~180–230 ms~15–25 ms~55–80 ms
Classical verification—P99~330–400 ms~25–40 ms~120–160 ms
Hybrid (with ML-DSA WASM)—P50~280–350 ms~50–70 ms~110–140 ms
Hybrid (with ML-DSA WASM)—P99~550–700 ms~100–140 ms~240–300 ms
Warm hybrid—P99~15–25 ms~5–15 ms~10–20 ms

Three observations, before the engineering details:

  • Cloudflare Workers' isolate model is the only one where pure platform boot is effectively free. A Worker is a V8 isolate spun up inside an already-running process; there is no container start, no language runtime initialization, no JIT warm-up of the host VM. The cost shows up later, in WASM instantiation, but it does not show up at the platform layer.
  • AWS Lambda's cold-start tail is the longest and the widest. A Node.js Lambda spends 80–200 ms on container start plus runtime init before user code runs at all. With WASM in the path, the P99 reaches 600 ms territory. This is the cost Provisioned Concurrency exists to delete.
  • Deno's cold start sits between the two but is more predictable. Supabase Edge Functions ride on Deno Deploy's isolate model, which is closer to Workers than to Lambda, but the V8 cold start is paid more visibly because of how the runtime is structured.

The warm hybrid row is the one that matters for steady-state production: on every runtime, a warmed instance verifies a hybrid signature in well under 20 ms. The entire engineering problem is keeping warmed instances available, and compressing the cost when a cold start is unavoidable.

The cold-start budget

A cold-start budget on a serverless identity function decomposes into five buckets, all of which can be measured independently:

BucketTypical share of cold startKnobs that move it
Platform boot (container/isolate)5–60%Choice of runtime; Provisioned Concurrency on Lambda
Runtime init (Node/Deno/V8)5–30%Bundle size; SnapStart on Lambda Java/.NET
Module imports + side effects10–35%Lazy imports; top-level await elimination
WASM instantiation5–40%Streaming compilation; module caching; bundle splitting
First DB connection / TLS handshake5–25%Connection poolers (PgBouncer, Hyperdrive); regional placement

The decomposition is what makes the optimization tractable. A team that benchmarks only the round-trip latency has one number; a team that benchmarks each bucket can attack the dominant cost first and stop when the budget is met.

Strategy 1: Lazy crypto library loading

The default pattern in most identity functions is to import every crypto library at the top of the file. On a cold start, every byte of every library is parsed and compiled before the request handler runs—even if the request only needs Ed25519 verification.

Lazy loading inverts this. The handler imports only the runtime's native primitives synchronously; PQC libraries are loaded on first use via dynamic import() and cached on the isolate-global scope thereafter.

// Module-level: small, always needed.
import { verifyEd25519 } from "./crypto-native.ts";

// Lazy: only instantiated on first request that needs it.
let mldsaPromise: Promise<typeof import("./crypto-mldsa.ts")> | null = null;
function getMldsa() {
  if (!mldsaPromise) mldsaPromise = import("./crypto-mldsa.ts");
  return mldsaPromise;
}

Deno.serve(async (req) => {
  const { sig, msg, scheme } = await req.json();
  if (scheme === "ed25519") return ok(verifyEd25519(sig, msg));
  const { verifyMldsa } = await getMldsa();
  return ok(await verifyMldsa(sig, msg));
});

In practice, lazy loading typically removes tens of milliseconds from the classical-only cold start on Deno-class runtimes, and a larger slice (often 50–100 ms) from the hybrid path on Lambda where module parsing and JIT warm-up are most expensive. The cost is one additional async round-trip on the very first hybrid request after a cold start; the benefit is that classical-only requests never pay for code they do not need.

The same pattern applies to anything large and conditional: tenant-specific configuration parsers, format-specific JWT decoders, optional observability shims. The rule is "import at the top of the file only what every request needs".

Strategy 2: Pre-warmed connection pools

A per-isolate Postgres client opening a fresh TCP connection on every cold start spends 40–80 ms on the TLS handshake alone—before the first query runs. Multiplied across an isolate fleet under load, this is a permanent tax on the cold-start tail.

The fix is to move the pool outside the function. Three options, with trade-offs:

OptionWhere the pool livesCold-start costNotes
PgBouncer (transaction mode)A long-running pooler near the databaseTLS handshake only to PgBouncerStandard for Supabase, RDS Proxy, Neon
Cloudflare HyperdriveCloudflare's edge networkSub-10 ms warm handshakeWorkers-specific; caches the TLS session
Direct connection with cachingInside the isolateFull handshake on every cold startOnly safe when isolates are reused aggressively

For Supabase Edge Functions and AWS Lambda, the pooler-in-the-middle pattern is the standard answer. For Cloudflare Workers, Hyperdrive collapses the TCP+TLS+auth round-trips to a single edge hop and caches the result across invocations within the same isolate, which is the most aggressive cold-start optimization for database-fronted Workers in 2026.

PasskeyBridge's verification path uses Supabase's transaction-mode pooler for any function that needs a Postgres lookup, and treats direct connections as an anti-pattern for the edge tier.

Strategy 3: WASM-compiled PQC with streaming instantiation

ML-DSA and ML-KEM in 2026 are predominantly shipped as WASM modules. Native bindings exist for server runtimes, but they do not exist for V8 isolates or for Deno Deploy's edge tier; WASM is the lingua franca. The module is also the dominant cost on a cold hybrid path: a 480 KB WASM blob takes 18–55 ms to instantiate depending on runtime.

Three techniques compound to reduce this:

  1. WebAssembly.instantiateStreaming. Stream the WASM bytes directly from a Response into the compiler, overlapping the network fetch with the compilation pass. On Cloudflare Workers, the WASM is loaded directly from the bundle and instantiation begins as soon as the first chunk arrives.
  2. Isolate-global caching. Cache the instantiated WebAssembly.Module on a module-level binding. The first request pays the cost; every subsequent request on the same isolate gets a free new WebAssembly.Instance(cachedModule), which is sub-millisecond.
  3. Bundle splitting. Ship classical-only and hybrid code in separate chunks. Cold starts that only need classical verification never compile the PQC module at all.
// One-time per isolate. Streaming + global cache.
let mldsaModule: WebAssembly.Module | null = null;
async function getMldsaModule() {
  if (mldsaModule) return mldsaModule;
  const res = await fetch(new URL("./mldsa.wasm", import.meta.url));
  const { module } = await WebAssembly.instantiateStreaming(res, imports);
  mldsaModule = module;
  return module;
}

In practice, all three techniques together typically cut hybrid-path P99 by roughly two-thirds on Lambda and by half on Cloudflare Workers. Once the isolate is warm, the instantiation cost is amortised to effectively zero. For the underlying architectural decisions, see our piece on why ML-DSA-65 is the default signature scheme for the PasskeyBridge edge tier.

Strategy 4: Provisioned and synthetic pre-warming

The runtime-level fix for cold starts is to never let them happen. Two patterns:

AWS Lambda Provisioned Concurrency. A configured number of instances are kept initialized and idle, ready to serve requests with no platform-boot cost. AWS documents that provisioned concurrency removes init duration from the request path entirely; for the hybrid identity workload that is typically a 90%+ reduction at P99. The cost is paid for idle capacity, which only makes sense when traffic is steady enough to keep the instances utilized.

Synthetic heartbeats. Where Provisioned Concurrency is not available—Cloudflare Workers, Supabase Edge Functions—a low-frequency synthetic request from a known source can keep at least one isolate warm in each region. The PasskeyBridge edge tier uses a sub-minute heartbeat against a sentinel path that exercises the same code paths as a real verification, with a dedicated synthetic header that excludes the traffic from production metrics so heartbeat traffic does not pollute the DPoP shadow telemetry and other observability layers that consume it.

Heartbeats are not free—they consume compute capacity—but the cost is bounded and predictable, and the latency floor they buy on the user-facing path is what keeps a cold start from landing on a real sign-in.

The combined impact

Stacking all four strategies on the hybrid workload typically lands each runtime in the following neighbourhood:

RuntimeNaive cold P99+ Lazy loading+ Pooled connections+ WASM streaming/cache+ Pre-warming
AWS Lambda~600 ms~520 ms~470 ms~200 ms~45–60 ms
Cloudflare Workers~120 ms~100 ms~70 ms~50 msn/a (always warm via isolate model)
Supabase Edge Functions~270 ms~220 ms~170 ms~90 ms~35–45 ms (via synthetic heartbeat)

The headline shape is the same on every runtime: an order-of-magnitude improvement is available, and almost all of it comes from the same four primitives applied in roughly the same order. None of the techniques is novel in isolation; the compound effect is what makes the budget realistic.

Consequences for an identity SLO

The platform you pick matters less than how you instrument the path. A Lambda deployment without Provisioned Concurrency, lazy loading, pooled connections, and cached WASM modules will miss a sub-100 ms P99 target by a wide margin on every cold hit. The same deployment with all four lands inside the budget. A Workers deployment can hit the budget with fewer levers, but the levers are still required—particularly WASM streaming, which is what separates a roughly 120 ms cold P99 from a sub-60 ms one.

The architectural commitment for an edge-first identity verification stack is that none of these techniques is optional. They are the floor. The interesting work happens above the floor: DPoP token binding, SSE/CAEP cross-IdP revocation, carrier signal freshness. None of those features is meaningful if the verification function spends 600 ms parsing crypto libraries before it can answer.

Position in 2026

Serverless identity is the workload that exposes every assumption a runtime makes. Cold-start engineering for it is no longer a tail-latency footnote; it is the gating constraint on whether a serverless architecture can serve identity at all. The combination of V8 isolates, streaming WASM compilation, edge-resident connection pools, and synthetic pre-warming is what makes the answer yes—and what lets PasskeyBridge serve hybrid classical-plus-PQC verification on a fully serverless control plane without a cold start landing on a sign-in.

The runtime you pick is a constraint. The cold-start budget you hit is a choice.

Start free · Test the API