Serverless has become the default starting point for new backend work on our client projects, but which platform to standardize on has been a recurring internal debate for over a year. We finally settled it, at least for our own purposes, by running identical Node.js APIs on Google Cloud Run and AWS Lambda and comparing cold start behavior under traffic patterns modeled on a real client API rather than a synthetic benchmark.
How we ran the comparison
Both services ran the same Express application, wrapped for Lambda using a standard adapter and deployed as a container image on Cloud Run, with identical memory allocation and the same dependency set on both sides. We simulated a traffic pattern with long idle gaps followed by sudden bursts, since cold starts only matter when an instance actually has to spin up from zero, and a benchmark that keeps functions warm the whole time tells you nothing useful about the failure mode people actually care about.
Cloud Run's container-based model consistently showed shorter cold starts for our Node API than Lambda's zip-based deployment, on the order of a few hundred milliseconds faster at the p50 and a more pronounced gap at the p95, likely because our container image was already tuned for fast startup with a slim base image and minimal dependency footprint. Lambda's cold start penalty grew noticeably worse once we added a VPC connector to reach a private database, a well-known Lambda gotcha that added a meaningful delay to cold invocations; Cloud Run's networking model didn't carry the same penalty in our tests.
Where each platform pulled ahead on things other than raw latency
Lambda's ecosystem integrations, especially with API Gateway, EventBridge, and S3 event triggers, are more mature and saved us real development time on anything event-driven, where a function needs to react to a queue message or a storage event rather than serve an HTTP request directly. Wiring the equivalent event-driven pattern together on Google Cloud meant reaching for Pub/Sub and Cloud Functions instead of Cloud Run, which works but meant Cloud Run itself wasn't really a fair comparison for that category of workload in the first place.
- Cloud Run showed shorter cold starts than Lambda for our containerized Node API, with the gap widening once a VPC connector was involved on the Lambda side.
- Lambda's provisioned concurrency and Cloud Run's minimum instance settings both effectively eliminate cold starts for a cost, and we measured that cost as roughly comparable between the two once traffic volume was accounted for.
- Lambda's event-driven integrations with API Gateway, EventBridge, and S3 are more mature than the equivalent Google Cloud event patterns we tested.
- Deployment velocity favored Cloud Run for our team specifically, since a container image is a format every engineer already understood, while Lambda's packaging and layer conventions needed more onboarding.
Cost and the provisioned-concurrency tradeoff
Both platforms offer a way to pay your way out of cold starts entirely, provisioned concurrency on Lambda and minimum instance counts on Cloud Run, and we tested both to see whether the cold start advantage we measured actually mattered once cost was on the table. For a client API with predictable daytime traffic and a genuine quiet period overnight, keeping one warm instance running around the clock on either platform cost roughly the same, low double-digit dollars a month at the traffic volumes we modeled, which made the decision less about cost and more about which platform's warm-instance behavior was easier to reason about operationally.
Runtime choice mattered almost as much as platform choice
Part of what made this comparison harder than a simple platform-versus-platform benchmark is that cold start behavior varies a lot by language runtime, and we didn't want to draw a conclusion about Cloud Run versus Lambda that was really a conclusion about Node.js specifically. We ran the same comparison with a small Go service on both platforms, since Go's fast startup and small binary size are often cited as the ideal case for minimizing cold starts, and the gap between Cloud Run and Lambda narrowed considerably; both platforms handled the Go service's cold starts well enough that the difference was closer to noise than the more pronounced gap we saw on the Node service. That's a useful data point on its own: teams chasing cold start improvements should look at their runtime and dependency footprint before assuming a platform migration is the answer, since a leaner runtime closed most of the gap that a platform switch would have otherwise been asked to solve.
Observability and the deployment pipeline
Cold starts are also easier to reason about when you can actually see them clearly in your monitoring, and the two platforms differ here more than we expected going in. Cloud Run's request logs surface a clear "instance starting" marker that's easy to correlate with a slow request, while identifying a Lambda cold start from CloudWatch logs alone required parsing the `REPORT` line for the `Init Duration` field, which isn't present on warm invocations and took our team longer to build reliable dashboards around. On the deployment side, Cloud Run's container-based model meant we could reuse the exact same Docker image and CI pipeline we already use for our container-based backend services, while Lambda's zip-based packaging, or its own container support which we also tested, needed a separate build step tailored to Lambda's specific packaging expectations. Neither is a dealbreaker, but the Cloud Run pipeline required noticeably less new CI configuration for a team that was already building containers for other services.
Memory allocation as a lever we hadn't fully explored
One variable we underweighted going into the benchmark was memory allocation, which affects CPU allocation on both platforms and turned out to have a bigger effect on cold start time than we expected on Lambda specifically. Bumping our Lambda function's memory allocation from the default 128 megabytes to 512 megabytes, well beyond what the function actually needed for its working memory, cut cold start time meaningfully, since Lambda allocates CPU proportionally to configured memory and a cold start's dominant cost is CPU-bound initialization work, not memory pressure. Cloud Run showed a smaller version of the same effect when we bumped its CPU allocation, but the effect size was less dramatic, likely because Cloud Run already defaults to a more generous CPU allocation than Lambda's lowest memory tier. Any team benchmarking cold starts on Lambda should treat memory allocation as a serious tuning knob before drawing conclusions about the platform itself.
Multi-region deployment changed the calculus
Everything above describes a single-region deployment, and the picture shifts once a service needs to run in multiple regions for latency or redundancy reasons. Cloud Run's regional model meant standing up the same service in a second region was a straightforward copy of the same container image with a new region flag, while Lambda's regional deployment required duplicating function configuration, IAM roles, and event source mappings for each new region, none of which was hard individually but added up to noticeably more infrastructure-as-code to maintain per additional region. For a client expecting to launch in a second region within the next year, we weighted this ease of multi-region expansion more heavily than the raw cold start numbers, since the operational cost of adding regions compounds over a service's lifetime in a way a single cold start benchmark doesn't capture.
Concurrency settings shaped cold start frequency more than raw speed
How many requests a single instance can handle concurrently turned out to matter as much as the speed of any individual cold start, since a higher concurrency-per-instance setting means fewer new instances need to spin up in the first place during a traffic burst. Cloud Run defaults to handling multiple concurrent requests per instance, up to a configurable limit, while a default Lambda function handles exactly one request per instance and relies on horizontal scaling, spinning up new instances, to absorb concurrent load. For a bursty traffic pattern specifically, that difference meant Cloud Run triggered noticeably fewer cold starts overall during a burst, not because any single cold start was avoided, but because fewer cold starts needed to happen at all under the same traffic shape.
A caveat about how we measured this
It's worth being upfront that our results reflect one traffic shape, one region, and one dependency footprint, and a team with a meaningfully different workload should treat these numbers as a starting point rather than a verdict to copy directly. A function with a much heavier set of npm dependencies, or one that needs to establish a database connection pool on cold start rather than lazily on first request, would likely see both platforms' cold start numbers shift, and possibly shift the gap between them too. We'd encourage any team making this decision to run a scaled-down version of this same benchmark against their actual workload rather than taking someone else's numbers, ours included, as gospel.
What we told the team to actually do with this
We turned the benchmark into a short internal decision guide rather than a single blanket recommendation, since the right answer clearly depended on workload shape more than either platform being categorically better. The guide walks a project lead through three questions: is the workload primarily HTTP request and response, is it already going to live inside an existing AWS or GCP footprint, and does it have latency-sensitive, bursty traffic likely to hit cold starts often. HTTP-heavy, latency-sensitive, greenfield workloads point to Cloud Run under that guide; anything event-driven or already embedded in an AWS ecosystem points to Lambda regardless of the cold start numbers, since the ecosystem fit outweighs a latency difference that provisioned concurrency or a memory bump can mostly close anyway.
For pure HTTP APIs where startup latency matters most and the workload doesn't naturally fit an event-driven pattern, Cloud Run has become our default recommendation for new client work, particularly for teams that are already comfortable with containers. For event-driven architectures already living in AWS, or for clients with existing Lambda infrastructure we'd be adding to rather than replacing, Lambda still wins on ecosystem fit, and we wouldn't recommend fighting that gravity just to chase a cold start number that provisioned concurrency, or a leaner runtime, can close anyway.