PasskeyBridge —

Engineering · 2026-05-26

Cold-Start Latency in Serverless Identity Functions: Causes and Mitigations

By J. W. Bouckaert

Cold-Start Latency in Serverless Identity Functions: Causes 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 fast 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.

This piece publishes no cold-start figures for these variants. We have not run this benchmark, and the platforms' own documentation does not cover a hybrid identity workload. For the platform layer, read each platform's own account: AWS Lambda's init-duration documentation, Cloudflare's V8-isolate cold-start post, and the Deno Deploy isolate model. What follows is how the three runtimes differ in structure, and why that shapes their cold starts.

Structural differences between the three runtimes

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 pays for container start plus runtime init before user code runs at all, and WASM in the path lengthens the tail further. 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.

Steady-state production runs on warmed instances, which skip every one of those startup costs. 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:

BucketKnobs that move it
Platform boot (container/isolate)Choice of runtime; Provisioned Concurrency on Lambda
Runtime init (Node/Deno/V8)Bundle size; SnapStart on Lambda Java/.NET
Module imports + side effectsLazy imports; top-level await elimination
WASM instantiationStreaming compilation; module caching; bundle splitting
First DB connection / TLS handshakeConnection 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 takes the PQC library's parse and compile time off the classical-only cold start on Deno-class runtimes, and saves more on 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 pays for a full TLS handshake before the first query runs. Multiplied across an isolate fleet under load, this adds a fixed cost to 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 networkOne edge hop to a warm, pooled connectionWorkers-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 WASM blob of hundreds of kilobytes has to be compiled and instantiated before the first hybrid verification.

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;
}

Together, the three techniques take most of the instantiation cost off the cold hybrid path. 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. 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

Each strategy removes a different bucket from the cold-start budget above: lazy loading takes module imports off the classical path, pooled connections take the handshake off the first query, WASM streaming and caching shrink instantiation, and pre-warming removes platform boot where the runtime allows it. Cloudflare Workers need the least pre-warming because the isolate model already makes platform boot cheap.

The shape is the same on every runtime: the improvement is large, 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 tight P99 budget 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 overlaps fetching the post-quantum module with compiling it instead of paying for the two one after the other.

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 its cold start 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