Serverless Cold Starts: Lambda SnapStart vs Cloudflare Workers vs Cloud Run (2026)
Every serverless platform makes the same bargain: you stop paying for idle capacity, and in exchange the platform occasionally has to build a runtime from nothing while a user waits. That wait is the serverless cold start, and it is the single most argued-about number in the category. The arguments are usually unproductive because the three leading platforms attack the problem at entirely different layers. AWS snapshots a booted microVM, Cloudflare avoids booting anything heavy at all, and Google lets you keep containers running and gives them extra CPU while they start.
If you pick a platform from a vendor’s headline claim, you will compare numbers that do not mean the same thing. If you pick it from a mechanism, you can predict where each one hurts: which language, which traffic shape, which dependency, which bill.
This guide dissects the cold start path on all three platforms using each vendor’s own documentation, then gives you a measurement harness you can run yourself and a decision matrix. Every number we quote is sourced; every number we invent for illustration is labelled.
What this covers: what a cold start physically consists of, Lambda SnapStart and provisioned concurrency, Cloudflare Workers isolates and warm-routing, Cloud Run minimum instances and startup CPU boost, a reproducible benchmark method, failure modes, and a decision matrix.
Context and Background
A cold start is the extra latency a request pays when no already-initialized execution environment is available to serve it. The platform has to find capacity, fetch your code or image, start an isolated environment, boot a language runtime, and run your top-level initialization code before the handler can run. Everything after that is a warm path, and warm paths on all three platforms are fast and boring.
AWS describes the phases precisely. Lambda’s init phase performs three tasks: starting extensions, bootstrapping the runtime, and running the function’s static code. The documentation states that cold starts typically occur in under 1% of invocations, with a duration that varies from under 100 ms to over 1 second, and that cold starts are more common in development and test functions because those are invoked less often (AWS Lambda execution environment lifecycle). That is AWS’s own framing, so treat it as a typical-case statement, not a guarantee for your function.
Two things follow. First, the rate of cold starts and the duration of each are independent variables. A platform can make cold starts rare (Cloudflare’s routing work), make each one shorter (SnapStart), or remove them for a paid-for slice of traffic (provisioned concurrency, minimum instances). Second, the p50 latency of your service is almost unaffected by cold starts when they are under 1% of traffic. They live in your p99 and p99.9, which is exactly where service-level objectives and user complaints live.
The workload type decides whether any of this matters. A background queue consumer can absorb a two-second init without anyone noticing. A synchronous checkout API or an IoT command acknowledgement path cannot. If you are building the latter on event-driven infrastructure, our write-up on WebAssembly at the edge and the WASI runtime model covers the lighter-weight runtime family that Workers belongs to, and the Sparkplug B edge network architecture shows where bursty device traffic produces the scale-from-zero patterns that expose cold starts first.
One more framing before the mechanics. Cold start cost has three components that engineers routinely conflate:
- Platform time: locating capacity, pulling code, creating the sandbox. You mostly cannot change this, only avoid it.
- Runtime time: booting a JVM, CPython or the .NET runtime. You can change it by choosing a runtime, or by snapshotting it.
- Application time: your imports, framework wiring, connection pools and config loading. This is usually the largest piece, and the only one fully under your control.
AWS agrees with that last point: the largest contributor to startup latency is the time Lambda spends initializing the function, including loading code, starting the runtime and running initialization (Improving startup performance with Lambda SnapStart). Keep the three-part split in mind; every mitigation below attacks a different slice of it.
Three Architectures for Hiding Startup Cost
The three platforms embody three different theories of how to hide startup latency. Lambda SnapStart bets on checkpoint and restore: do the expensive initialization once, at deploy time, and resume from a saved memory image. Cloudflare Workers bets on cheap isolation: make creating an execution context so light, and route traffic so consistently, that most requests never see a boot. Cloud Run bets on keeping containers alive and starting them faster: it runs ordinary OCI containers, so it offers minimum instances and a CPU boost during startup.
Short answer: which approach has the lowest cold start? Workers generally has the smallest cold start because it starts a V8 isolate instead of a microVM or container, and Cloudflare reports warm-request rates of 99.99% for enterprise traffic. SnapStart narrows the gap for Java, Python and .NET on Lambda by restoring from a snapshot. Cloud Run is the most flexible but needs minimum instances to avoid zero-to-one starts.

Figure 1: Three ways to hide a serverless cold start. Lambda restores a snapshot, Workers starts an isolate, Cloud Run keeps containers warm and boosts CPU.
Figure 1 places the three side by side. The vertical axis in each lane is “what must happen before your handler runs,” and the lanes differ in how much of that work moves before the request arrives. In Lambda SnapStart, init is moved to publish time. In Workers, most of it is moved to the TLS handshake and to a long-lived pool of warm isolates. In Cloud Run, it is moved to a standing instance you pay to keep.
Lambda SnapStart: initialize once, resume many times
Lambda runs functions in Firecracker microVMs. With SnapStart enabled, Lambda initializes your function when you publish a version, takes a Firecracker snapshot of the memory and disk state of the initialized execution environment, encrypts it, and caches it. Lambda maintains several copies of each snapshot for resiliency and patches them with runtime and security updates. When the function is invoked and as it scales out, new execution environments are resumed from the cached snapshot instead of being initialized from scratch (AWS SnapStart docs).
The documentation currently lists support for Java 11 and later, Python 3.12 and later, and .NET 8 and later, across ZIP and container image packaging. It explicitly says other managed runtimes, naming nodejs24.x and ruby4.0, and OS-only runtimes are not supported. SnapStart also does not combine with provisioned concurrency, Amazon EFS, or ephemeral storage above 512 MB, and it applies only to published versions and aliases, not $LATEST. Regional availability covers all commercial Regions except Asia Pacific (New Zealand) and Asia Pacific (Taipei). Support lists change, so verify them against the current page before committing.
The headline claim is careful: SnapStart “can provide as low as sub-second startup performance,” and AWS says it reduces latency from “several seconds to as low as sub-second, in optimal scenarios.” It also notes that SnapStart works best at scale, and that infrequently invoked functions might not see the same improvement. That caveat is easy to miss and has a mechanical explanation we return to in the failure-modes section.
Cloudflare Workers: isolates and warm routing
Workers do not run in a microVM per function. They run in V8 isolates inside a shared runtime process, which makes the creation of a new execution context far cheaper than creating a container. Cloudflare’s 2020 post on eliminating cold starts described the first big trick: preload the Worker based on the TLS Server Name Indication (SNI) hostname in the ClientHello, so the Worker warms while the TLS handshake is still in flight. The post states that a Worker can be warmed in under 5 milliseconds, shorter than the client-to-edge round trips of the handshake, hence the “zero cold start” claim (Cloudflare: Eliminating Cold Starts with Cloudflare Workers).
The second trick, published in September 2025, attacks the cold start rate. Cloudflare hashes each Worker’s script ID onto a consistent-hash ring so that requests for the same Worker, arriving at different servers in a data center, are forwarded to one “home” server. Cloudflare reports that the warm request rate for enterprise traffic rose from 99.9% to 99.99%, a tenfold cut in cold starts, and that global Worker evictions dropped by the same factor (Cloudflare: Eliminating Cold Starts 2: shard and conquer). Only about 4% of enterprise requests needed to be sharded, which Cloudflare attributes to the power-law distribution of traffic.
Note what the numbers claim and what they do not. A 99.99% warm rate means roughly one request in ten thousand is cold. It does not say that a cold request costs zero. Cloudflare’s own definition of a cold start includes fetching source, compiling, executing top-level code and the first invocation, and it admits the early “5 ms” claim was true of that time, not a standing guarantee.
Cloud Run: containers, minimum instances and startup CPU boost
Cloud Run runs your container image, so its cold start includes scheduling an instance, pulling the image layers if not cached, starting the container and waiting for it to listen on the port. Google offers two direct levers. Minimum instances keep a set number of instances running even when idle, with a default of 0. The documentation calls this a best-effort target and recommends at least three minimum instances for high availability (Cloud Run minimum instances).
Startup CPU boost temporarily raises CPU during instance startup and for ten seconds afterwards. Per the CPU documentation, a container with a CPU limit of 0 to 1 is boosted to 2 CPUs, a limit of 2 to 4, a limit of 4 to 8, and limits of 6 and 8 to 8. You are billed for the boosted CPU for the startup period (Cloud Run CPU configuration). When Google announced the feature in 2022, it reported internal and preview results of up to 50% shorter startup for a Spring PetClinic sample, up to 47% for a native Spring GraalVM service, up to 23% for plain Java Cloud Functions, and up to 30% for Node.js in private preview, noting that results vary by language and framework (Google Cloud: startup CPU boost). Those are vendor-reported best cases from 2022, not a prediction for your service.
Anatomy of a Cold Start and How Each Platform Cuts It
To compare mitigations you need a shared timeline. The sequence below is generic; each platform skips or relocates different steps.

Figure 2: Cold start timeline. SnapStart replaces runtime and init with a restore, Workers shrinks the whole path to an isolate, Cloud Run keeps an instance waiting.
Figure 2 follows one request through a cold path. The platform front end receives it, finds no idle environment, and asks a placement service for capacity. Capacity is created, code or image is fetched, the runtime boots, application init runs, and only then does the handler see the request. The mitigations differ in which of those steps they delete from the request’s critical path.
Where the milliseconds go
On Lambda, AWS lists the factors that determine init latency as the size of the package including libraries and layers, the amount of init code, and the performance of libraries and services at connection setup. The practical effect is that a Java function with a dependency-injection framework and a large SDK client graph spends its init budget on class loading and reflection, while a Python function spends it on imports. A Go or Rust function on a custom runtime has almost nothing to initialize, which is why many teams find that runtime choice moves cold start more than any platform feature.
On Cloud Run, a new instance also pays for image acquisition and for the container’s own startup. Because Cloud Run runs standard containers with up to 8 vCPU per instance, the boost table above matters most for runtimes that parallelize startup work: JIT compilation threads in the JVM, class scanning, ahead-of-time cache warming. Google’s own announcement says Java benefits most and Node.js gains are smaller because it is single-threaded.
On Workers, there is no image and no process to boot. What remains is fetching the script if the isolate pool does not hold it, compiling it, running top-level code and the first invocation. That is why Cloudflare raised its startup CPU limit to 400 ms and its compressed script size limit to 10 MB on paid plans: both bound exactly the work that a cold instantiation has to do. A Worker that parses a 6 MB bundle at the top level will have a cold start proportional to that parse, no matter how good the routing is.
Lambda: SnapStart versus provisioned concurrency
Lambda gives you two separate tools, and they solve different problems. Provisioned concurrency keeps a configured number of execution environments initialized and ready, designed for double-digit millisecond responses. Lambda runs your init code when the environment is allocated and again whenever it recycles instances, and bills for that init even if the environment never serves a request. It is billed continuously, separate from invocation charges.
SnapStart does not keep environments alive. It makes creating a new one cheaper. The restore phase resumes from the snapshot; if you registered an after-restore hook, that code runs at the end of the restore phase and you are charged for its duration. Restore must complete within a 10-second limit or you receive a SnapStartTimeoutException.
AWS’s own guidance draws the line clearly: if your application has strict cold-start latency requirements that SnapStart cannot adequately address, use provisioned concurrency. And they cannot be stacked, since SnapStart does not support provisioned concurrency.
The cost structure differs sharply. For Java managed runtimes there is no additional charge for SnapStart; you pay the usual requests, duration and memory. For Python and .NET, you pay a caching charge for every published version with SnapStart enabled (minimum of three hours, continuing while the version stays active) plus a restoration charge each time an environment resumes, both scaled by configured memory. Duration charges also cover init code outside the handler and runtime hooks. Delete unused versions, because the cache charge accrues per version.
Here is a SAM template that enables SnapStart on a Python function and a handler that uses the documented hooks:
# template.yaml (AWS SAM) - SnapStart on a published version
AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31
Resources:
Api:
Type: AWS::Serverless::Function
Properties:
Handler: app.lambda_handler
Runtime: python3.13
MemorySize: 1024
Timeout: 10
AutoPublishAlias: live # SnapStart needs a published version
SnapStart:
ApplyOn: PublishedVersions
Environment:
Variables:
POWERTOOLS_SERVICE_NAME: demo
# app.py - restore-safe initialization with SnapStart hooks
import os, time, random, json
from snapshot_restore_py import register_before_snapshot, register_after_restore
T_INIT = time.time() # captured in the snapshot
_conn = None # lazily (re)created after restore
_rng = random.Random()
def _connect():
# Placeholder for a DB or HTTP client. Replace with your real client.
return {"opened_at": time.time()}
@register_before_snapshot
def before():
global _conn
_conn = None # never snapshot live sockets
@register_after_restore
def after():
global _conn, _rng
_rng = random.Random(os.urandom(16)) # restore uniqueness
_conn = _connect()
def lambda_handler(event, context):
global _conn
if _conn is None:
_conn = _connect()
return {
"statusCode": 200,
"body": json.dumps({
"init_age_s": round(time.time() - T_INIT, 3),
"rand": _rng.random(),
}),
}
The os.urandom reseed is the important detail. AWS warns that anything generated during init and captured in the snapshot, including unique IDs, secrets and entropy for pseudorandomness, will be identical across every environment restored from that snapshot. The Python and Java runtimes handle some cases for you, but the documentation tells you to verify rather than assume.
Cloud Run: deploying the levers
On Cloud Run, the equivalent knobs are flags on the service:
# Keep warm capacity and boost CPU during startup
gcloud run deploy api \
--image us-docker.pkg.dev/PROJECT/repo/api:latest \
--region us-central1 \
--cpu 1 --memory 1Gi \
--min-instances 2 \
--cpu-boost \
--concurrency 80
Minimum instances remove only the zero-to-one cold start. They do nothing for scale-out: when traffic rises above what the warm instances can serve at the configured concurrency, new instances start cold. Google’s announcement makes the same point, listing three triggers for cold starts: scaling from zero, a concurrency setting of one, and traffic scaling. Billing also differs by CPU mode. Under request-based billing, idle minimum instances are charged at a lower rate; under instance-based billing you pay the full rate for the whole lifecycle even with a minimum of zero.
Concurrency interacts with cold start in a way people overlook. Cloud Run can send many concurrent requests to one instance, so a burst of 80 requests may land on a single warm instance and produce no cold starts, whereas Lambda gives each in-flight request its own execution environment by default. The same traffic spike therefore causes up to 80 Lambda inits but possibly zero Cloud Run starts. That structural difference often outweighs any per-start duration comparison.
Measuring Cold Starts Honestly
Published comparisons of cold starts are mostly worthless for your decision, because they measure someone else’s code, region, memory size and traffic. The correct deliverable is a method you can rerun. Below is one that works on all three platforms, with a design that separates server-observed cold starts from client-observed latency.

Figure 3: Measurement harness. A client sends idle-gap probes and bursts; each target reports whether it served its first request.
Methodology
Three rules keep the data useful. First, tag cold requests on the server: the platform knows better than your client whether this was the first request on a new environment. A module-level counter incremented per request does it. Second, measure from outside the provider with a client in the same region (or in the region of your users), so you capture network and front-end time, not only function duration. Third, report percentiles over hundreds of samples and always include the cold-only distribution separately from the overall one.
Use two probes. The idle-gap probe waits long enough for environments to be reclaimed and then sends one request, repeated many times; this measures the zero-to-one case. The burst probe sends N simultaneous requests; this measures scale-out cold starts and exposes the concurrency-model difference. Because retention periods are not guaranteed by any of the three vendors, you cannot assume a fixed idle time. AWS explicitly says Lambda terminates environments every few hours even for continuously invoked functions, and does not document a fixed idle retention window. Let the server-side flag, not the clock, decide which samples count as cold.
The same tiny handler logic goes on each platform. On a Cloud Run container or a Workers script it is a few lines:
# cloud_run_app.py - Flask; deploy as a container to Cloud Run
import time, os, threading
from flask import Flask, jsonify
START = time.time()
_lock = threading.Lock()
_count = 0
app = Flask(__name__)
@app.get("/")
def root():
global _count
with _lock:
_count += 1
n = _count
return jsonify(first_request=(n == 1),
instance_age_s=round(time.time() - START, 3),
pid=os.getpid())
// worker.js - Cloudflare Worker with the same probe contract
let count = 0;
const START = Date.now();
export default {
async fetch(request) {
count += 1;
return Response.json({
first_request: count === 1,
isolate_age_ms: Date.now() - START,
});
},
};
Two cautions about the Worker probe. Isolate state is not a stable identity: Cloudflare may run your Worker in many isolates and evict them, and Date.now() in Workers only advances at I/O boundaries as a security measure, so treat isolate_age_ms as coarse. And a Worker’s “first request” flag measures isolate instantiation, not the TLS handshake the preload trick hides, which is why the client-side time-to-first-byte is the number to use.
Now the client. It uses only the standard library, so it runs anywhere:
# probe.py - idle-gap and burst probes with server-side cold tagging
import json, sys, time, statistics, urllib.request
from concurrent.futures import ThreadPoolExecutor
def hit(url):
t0 = time.perf_counter()
with urllib.request.urlopen(url, timeout=30) as r:
body = json.loads(r.read())
return (time.perf_counter() - t0) * 1000, body
def pct(xs, p):
xs = sorted(xs)
return xs[min(len(xs) - 1, int(round(p / 100 * (len(xs) - 1))))]
def summarize(label, samples):
cold = [ms for ms, b in samples if b.get("first_request")]
warm = [ms for ms, b in samples if not b.get("first_request")]
print(f"{label}: n={len(samples)} cold={len(cold)} warm={len(warm)}")
for name, xs in (("cold", cold), ("warm", warm)):
if xs:
print(f" {name}: p50={pct(xs,50):.0f}ms p95={pct(xs,95):.0f}ms "
f"p99={pct(xs,99):.0f}ms max={max(xs):.0f}ms")
def burst(url, n):
with ThreadPoolExecutor(max_workers=n) as ex:
return list(ex.map(lambda _: hit(url), range(n)))
if __name__ == "__main__":
url, rounds, gap_s, n = sys.argv[1], int(sys.argv[2]), int(sys.argv[3]), int(sys.argv[4])
idle, bursts = [], []
for i in range(rounds):
idle.append(hit(url))
bursts.extend(burst(url, n))
print(f"round {i+1}/{rounds} done, sleeping {gap_s}s")
time.sleep(gap_s)
summarize("idle-gap", idle)
summarize("burst", bursts)
Run it as python probe.py https://YOUR-ENDPOINT/ 30 1200 50: thirty rounds, a 20-minute gap, a 50-wide burst each round. Because the harness tags cold requests server-side, a gap that happens to be too short for reclamation simply yields fewer cold samples rather than corrupting the result.
For Lambda, you can also force cold starts deterministically by changing an environment variable, which makes Lambda discard existing environments for that configuration, and then read the Init Duration or Restore Duration from the invocation log tail:
# lambda_force_cold.py - force N cold starts and read platform-reported durations
import base64, re, time, boto3
lam = boto3.client("lambda")
FN, N = "my-fn", 20
def wait_active():
lam.get_waiter("function_updated_v2").wait(FunctionName=FN)
for i in range(N):
lam.update_function_configuration(
FunctionName=FN, Environment={"Variables": {"NONCE": str(time.time())}})
wait_active()
ver = lam.publish_version(FunctionName=FN)["Version"] # needed for SnapStart
wait_active()
r = lam.invoke(FunctionName=FN, Qualifier=ver, LogType="Tail", Payload=b"{}")
log = base64.b64decode(r["LogResult"]).decode()
m = re.search(r"(Init|Restore) Duration: ([\d.]+) ms", log)
d = re.search(r"\bDuration: ([\d.]+) ms", log)
print(ver, m.group(0) if m else "warm?", d.group(0) if d else "")
Treat the output as platform-reported time, which excludes network and front-end overhead; pair it with the client-side probe for the full picture. AWS documents that functions with SnapStart or provisioned concurrency always emit an INIT_REPORT log line, and that SnapStart emits RESTORE_REPORT, so these two fields are the supported way to separate restore cost from init cost.
Reading the results
Do not average. Report cold-only p50, p95 and max, the cold fraction, and the warm p99. Then multiply: expected extra p99 latency for your traffic is roughly cold fraction times cold penalty, and your SLO tolerates it or it does not. As a purely illustrative calculation (not a measurement): at 0.01% cold rate and a 1.5 s cold penalty, one request in ten thousand is slow, which shows up at p99.99 but is invisible at p99. At a 3% cold rate with a 600 ms penalty, p99 is visibly slower even though each cold start is shorter. Rate and duration trade against each other, and the three platforms optimize different sides of that trade.
Choosing a Platform: Decision Matrix and Cost Logic
The right question is not “which platform has the fastest cold start” but “which cold start profile fits my runtime, my traffic shape and my budget.” The matrix below summarizes the documented properties rather than measured speeds, because speeds depend on your code.
| Dimension | Lambda + SnapStart | Lambda + provisioned concurrency | Cloudflare Workers | Cloud Run + min instances + CPU boost |
|---|---|---|---|---|
| Unit of execution | Firecracker microVM restored from snapshot | Pre-initialized microVM | V8 isolate | OCI container |
| Languages | Java 11+, Python 3.12+, .NET 8+ (per current docs) | All managed runtimes and custom | JavaScript, TypeScript, WebAssembly-targeting languages | Anything in a container |
| What it removes | Runtime boot and init on the request path | Cold start for the provisioned slice | Most boots, via preload and warm routing | Zero-to-one start for the minimum slice |
| What it does not remove | Restore cost, scale-out restores | Spillover beyond provisioned level | Cold instantiation of large scripts | Scale-out starts above warm capacity |
| Extra cost | Free for Java; cache plus restore for Python and .NET | Continuous per-unit charge | None for the preload | Idle minimum instances billed (lower rate under request billing) |
| Concurrency model | One request per environment | One request per environment | Many requests per isolate | Many requests per instance |
| Main risk | Uniqueness and stale connections after restore | Paying for idle capacity | Runtime and API constraints | Heavy images, slow init under burst |
Read the matrix by rows of constraint. If you are locked into Node.js on AWS, SnapStart is not on the table, so the realistic options are provisioned concurrency, a lighter bundle, or a different runtime. If you are locked into a JVM framework, SnapStart for Java costs nothing extra and is the obvious first experiment. If your code is JavaScript and latency-critical at the edge, Workers’ economics are different because the mitigation is built into the platform. If you already ship containers with arbitrary dependencies, Cloud Run is the path of least friction.

Figure 4: Decision flow. Runtime constraint first, then traffic pattern, then cost of idle capacity.
Figure 4 formalizes that order. The runtime question comes first because it removes options outright. The traffic question comes second because it determines whether you can afford standing capacity. The cost question decides last.
A worked cost frame for keeping capacity warm
Provisioned concurrency and minimum instances convert a variable cold-start penalty into a fixed cost. The right number is derived from Little’s Law, which the AWS documentation also uses: concurrency equals average requests per second multiplied by average duration in seconds. AWS suggests adding a 10% buffer.
Take an illustrative service averaging 40 requests per second with a 250 ms average duration. Concurrency is 40 x 0.25 = 10, and with a 10% buffer you provision 11. Now suppose traffic doubles for ten minutes every hour. The extra 10 concurrent executions spill over to on-demand environments, which cold start, so your cold starts concentrate exactly at the peaks where users are most numerous. You can chase the peak with scheduled scaling or target tracking via Application Auto Scaling. AWS notes that target tracking needs sustained load: a burst must persist for at least three minutes, and the alarms need three data points, so short, sharp spikes may not trigger scaling at all. AWS suggests using the Maximum statistic instead of Average for bursty traffic.
For Cloud Run, the same arithmetic applies with concurrency per instance as a divisor. At 40 requests per second and 250 ms duration you have 10 requests in flight; with a container concurrency of 80, one instance can in principle hold that load, so a minimum of one or two instances covers it, and Google’s recommendation of at least three minimum instances is about availability, not capacity. A high concurrency setting makes minimum instances far cheaper than the equivalent Lambda provisioned concurrency for the same in-flight load, but only if your code is safe to run concurrently in one process.
Workers has no knob to buy. Pricing is per request and CPU time, and the cost of cold starts shows up as variance, not as a line item. That is a real advantage when traffic is spiky and a real limitation when you need a guarantee, since there is nothing to configure if a particular route is slow.
Edge cases that change the answer
Three scenarios reverse the intuitive choice. The first is very low traffic. A function invoked a few times a day will cold start on almost every call on Lambda or Cloud Run at zero minimum, and SnapStart’s benefit is smaller for infrequent functions per AWS’s own guidance. For such workloads, the cheapest fix is often to make init small rather than to buy capacity. The second is huge fan-out. A queue-triggered function that scales to hundreds of concurrent environments cold starts hundreds of times, so a restore-based approach may help far more than a single warm instance. The third is heavy dependencies. A machine-learning model loaded at import time may take many seconds on any platform; snapshots can capture it, but a snapshot of multiple gigabytes is subject to memory and storage limits that you should check for your configuration.
Trade-offs, Gotchas, and What Goes Wrong
Every mitigation here trades one risk for another. Knowing the failure modes in advance is the difference between a clean rollout and a production incident.
SnapStart restores state you did not mean to share. Because many environments resume from one snapshot, anything unique captured at init is no longer unique. Random seeds, UUIDs, session keys and cached timestamps all fall into this category. AWS tells you to generate unique content after initialization and to refresh ephemeral data. The mitigation is to use after-restore hooks and, in Java, to follow the CRaC-based hook guidance in the documentation. Test for it: invoke twenty restored environments and assert that your “unique” values are unique.
Network connections do not reliably survive restore. AWS states that the state of connections established during init is not guaranteed on resume, though connections made by the AWS SDK generally resume automatically. A pooled database connection opened at init and snapshotted will usually be dead or shared in surprising ways. Close it in a before-snapshot hook and reconnect lazily.
SnapStart is version-bound and has feature conflicts. It works only on published versions and aliases, not $LATEST; it cannot be combined with provisioned concurrency or EFS; and ephemeral storage above 512 MB is unsupported. Teams that deploy by overwriting $LATEST will see no effect and wonder why. Also remember that each published version with SnapStart on accrues cache charges for non-Java runtimes, so a CI pipeline that publishes a version per commit and never cleans up will grow a bill quietly.
Provisioned concurrency can be a false comfort. Spillover beyond the provisioned level goes to on-demand environments that cold start. You can detect it by reading the AWS_LAMBDA_INITIALIZATION_TYPE variable, which is either provisioned-concurrency or on-demand, and logging it. Also, your event source must point at the alias or version that carries the configuration; otherwise requests bypass your warm pool entirely.
Workers’ constraints are the price of the isolate. There is no arbitrary native code and no filesystem, and CPU time is limited per request. Startup work is bounded: Cloudflare has described a 400 ms startup CPU limit and a 10 MB compressed script limit on paid plans. A framework that does heavy work at module scope can blow through that and fail to deploy, which is a safer failure than a slow cold start, but still a failure. Workers also cannot hide the latency of what your code calls: a cold Worker calling a distant database is bottlenecked by the database round trip, not the isolate. Cloudflare’s routing improvements also depend on sharding, and the post notes that if a shard server is overloaded the client serves the Worker locally, so you can still see cold instantiations in multiple servers at once under load.
Cloud Run’s minimum instances are best-effort. Google says the count can briefly dip below the minimum during capacity shortages, rebalancing or crashes. Combined with scale-out starts, “we set min instances” is not the same as “we have no cold starts.” The CPU boost also costs money: you pay for the boosted CPU during startup and the ten seconds after, so a service that restarts often because of frequent deploys or aggressive autoscaling will see that on the bill.
Anti-patterns worth naming. Keep-warm pings that invoke the function every five minutes are a hack that warms only one environment and does nothing for concurrent bursts. Initializing every SDK client at import “just in case” bloats init for code paths that never use them; AWS itself recommends lazy initialization for objects used only on certain paths. Choosing memory by cost alone ignores that on Lambda, more memory buys more CPU and can shorten init enough to pay for itself, so measure rather than guess.
Practical Recommendations
Start with the cheapest lever, which is the code. Strip imports, defer rarely used clients, shrink bundles and images, and measure init time before and after. This helps on every platform, costs nothing, and often removes enough of the cold start that the platform feature becomes unnecessary.
Then choose by constraint. On Lambda with Java, Python 3.12+ or .NET 8+, enable SnapStart on a published alias, add restore hooks for uniqueness and connections, and compare the Restore Duration against your old Init Duration. If the result still misses your latency objective at the percentile you care about, add provisioned concurrency sized by Little’s Law with a 10% buffer, and remember the two do not stack on the same function. On Cloud Run, set minimum instances to cover steady-state traffic, enable CPU boost for JVM and similar multi-threaded startup, and raise container concurrency if your code is thread-safe. On Workers, keep module-scope work minimal and watch bundle size.
Finally, put the measurement harness in CI or a scheduled job. Platform behavior changes: runtime support lists grow, routing improves, defaults shift. A weekly probe with cold-only percentiles turns those changes into data.
Checklist before you ship:
- Measured cold fraction and cold-only p95 and p99 with server-side cold tagging, not averages.
- Init code audited: lazy clients, trimmed dependencies, no live connections captured in snapshots.
- SnapStart (if used): published alias, uniqueness test, restore hooks, version cleanup job.
- Warm capacity (if used): sized by concurrency = rate x duration plus 10%, with spillover monitored.
- Burst test run, not only idle-gap tests, because scale-out starts dominate real incidents.
- Cost line items reviewed: cache and restore charges, idle instance charges, boost charges.
Frequently Asked Questions
What is a serverless cold start?
A serverless cold start is the extra latency a request pays when the platform has no initialized execution environment ready. The platform must allocate capacity, fetch code or an image, start the sandbox, boot the language runtime and run your initialization code before the handler runs. Subsequent requests reuse the environment and are faster. Cold starts affect tail latency far more than median latency, which is why p99 and p99.9 are the right places to measure them.
Does Lambda SnapStart eliminate cold starts?
No. SnapStart shortens them by resuming from a snapshot taken when you publish a version, instead of initializing the function from scratch. AWS describes the result as sub-second startup in optimal scenarios, not zero, and says functions invoked infrequently might not see the same gains. Each new environment still incurs a restore phase. If you need consistently low latency, AWS recommends provisioned concurrency, which keeps initialized environments ready.
Do Cloudflare Workers really have zero cold starts?
Cloudflare’s position is that Workers cold starts are hidden, not absent. It preloads a Worker during the TLS handshake and routes requests for the same Worker to a consistent home server. In 2025 it reported a warm request rate of 99.99% for enterprise traffic, meaning about one request in ten thousand is cold. Large scripts or heavy top-level code still make an unlucky cold instantiation slower.
How do I reduce Cloud Run cold starts?
Set a minimum instance count so at least some instances stay running, enable startup CPU boost so new instances get extra CPU while starting, and keep your container image and initialization small. Minimum instances only cover the zero-to-one case; scale-out under load still starts new instances. Raising container concurrency, where your code is thread-safe, lets each warm instance absorb more traffic and reduces how often scale-out is needed.
Is provisioned concurrency better than SnapStart?
They solve different problems. Provisioned concurrency keeps initialized environments ready, aiming for double-digit millisecond responses, but you pay for it continuously and any overflow still cold starts. SnapStart makes creating new environments cheaper without standing capacity, but each start has a restore cost, and it supports only certain runtimes. They are mutually exclusive on one function, so choose based on your latency target, runtime and traffic.
Which serverless platform is best for low latency APIs?
It depends on the runtime and traffic shape, not a universal ranking. JavaScript APIs that can live within isolate constraints suit Workers. Container-based services with moderate traffic suit Cloud Run with minimum instances. JVM, Python or .NET services already on AWS suit Lambda with SnapStart, adding provisioned concurrency if the target demands it. Measure your own cold-only percentiles with the harness above before committing.
Further Reading
Internal:
- WebAssembly at the edge: WASI runtime architecture for the lightweight-runtime model behind isolate-style platforms.
- Sparkplug B edge network architecture for production for the bursty device-traffic patterns that stress scale-from-zero.
- Azure IoT Akri and Kubernetes resource interfaces for how container platforms expose device resources.
External:
- AWS Lambda SnapStart documentation for current runtimes, limits and pricing.
- Cloudflare: Eliminating Cold Starts 2, shard and conquer for the warm-routing design.
- Cloud Run minimum instances and CPU configuration for the Google levers.
By Riju — about
