GCP Cloud Run vs AWS Lambda for Bursty APIs: What Broke, What Held
We ran the same bursty checkout API on Cloud Run and Lambda for six months. Cold starts, concurrency, and billing quirks all bit us in ways the marketing pages don't mention.

We spent about six months running the same checkout API on both GCP Cloud Run and AWS Lambda, side by side, behind a weighted DNS split. The traffic pattern was ugly on purpose: long flat valleys, then 30x spikes when a partner sent a batch of webhooks. This is what actually broke, what held up, and where the pricing pages lied to us by omission.
The workload, honestly
Before the comparison means anything, here's the shape of the thing we were running:
- Node.js 20, roughly 40 MB of dependencies, a warm-path latency budget of 150 ms p95.
- Postgres over a pooled connection (PgBouncer on a small VM), Redis for idempotency keys.
- Steady state: 20 – 60 req/s. Spikes: 1,200 – 2,000 req/s for 90 seconds, several times a day.
- Payload sizes 2 – 12 KB, occasional 400 KB webhook replays.
We deliberately did not pick a workload that favoured either platform. No GPU, no long-running jobs, no WebSockets. Just a boring JSON API that had to survive uneven load without paging anyone.
Cold starts: the number everyone quotes wrong
Cold start conversations tend to be religious. Here's what we measured, framed as ranges because your mileage will vary with runtime, package size, and VPC config:
- Lambda (Node 20, 1024 MB, no VPC): cold init in the 250 – 500 ms range. With a VPC attached and a couple of SDK clients initialised eagerly, we saw 700 ms – 1.4 s.
- Cloud Run (Node 20, 1 vCPU, 512 MB, min instances 0): cold start 400 – 900 ms typically, occasional 2 s outliers when a new revision was scaling from zero under load.
The headline: Lambda without a VPC is faster to cold-boot than Cloud Run in our tests. But — and this matters — Cloud Run only cold-starts a container, not a request. One container serves many concurrent requests. Lambda cold-starts per concurrent execution.
That single behavioural difference dominated everything else.
Why per-instance concurrency changes the math
Cloud Run defaults to concurrency=80 per instance. So a spike from 20 req/s to 1,200 req/s might need 15 – 20 new containers, not 1,180 new ones. Lambda, by contrast, runs one request per execution environment. A 1,200 req/s spike with 200 ms responses means Lambda has to spin up on the order of 240 concurrent environments, each paying its own cold start tax.
We saw this clearly in the p99. During spikes:
- Cloud Run p99 stayed around 600 – 900 ms.
- Lambda p99 climbed to 1.8 – 3.2 s until provisioned concurrency kicked in.
Provisioned concurrency fixes this, but it also changes the economics — more on that below.
What broke on Lambda
Two real incidents, both worth telling.
Incident 1: connection storm. When a spike hit, Lambda scaled to ~300 concurrent environments. Each one opened a Postgres connection through PgBouncer. PgBouncer's max_client_conn was 500, which felt generous until we realised our other Lambdas shared the pooler. We hit the ceiling and started refusing connections. The fix was RDS Proxy for the Lambda path specifically, which added ~15 ms per query but survived the storm.
Incident 2: init code doing too much. A well-meaning engineer moved a schema validation compile step into module scope. On warm invocations, free. On cold starts during a spike, it added 180 ms to every new environment, and we were spinning up hundreds. The p95 during the next spike jumped from 140 ms to 480 ms and stayed there for two minutes. Lesson: your init code runs N times under load, where N is your concurrency. Treat it like a hot loop.
What broke on Cloud Run
Cloud Run wasn't drama-free either.
Incident 3: CPU throttling outside requests. Cloud Run by default only allocates CPU during request processing. We had a background metrics flush running every 10 seconds. During quiet periods, it silently stalled — the CPU was throttled to near-zero between requests. We didn't notice for a week because logs still flushed on the next request. Switching to --cpu-boost and --cpu-throttling=false (CPU always allocated) fixed it but roughly doubled the idle cost.
Incident 4: revision rollout during a spike. A deploy went out at exactly the wrong moment. Cloud Run's default traffic migration shifted 100% to the new revision immediately, and the new revision had to scale from zero while handling the spike. The p99 went to 4 seconds for about 40 seconds. Now we do gradual rollouts:
gcloud run services update-traffic checkout-api \
--to-revisions=checkout-api-00042-abc=10 \
--region=europe-west1
# watch metrics, then
gcloud run services update-traffic checkout-api \
--to-revisions=checkout-api-00042-abc=50
Boring, but boring is the goal.
The billing surprises
The pricing pages are technically accurate and practically misleading. Here's what we actually paid, roughly, for the six-month window (numbers rounded and rate-limited to protect the client):
- Lambda: compute was cheap on the flat periods. Provisioned concurrency to handle spikes without pain added about 35% to the bill. Data transfer and CloudWatch Logs added another surprise line item — Logs alone was ~18% of the Lambda total until we shipped to S3 via a subscription filter.
- Cloud Run: min-instances=1 (to avoid cold starts on the first request of the day) plus
--cpu-throttling=falseroughly tripled our baseline from the naive config. Still cheaper than Lambda + provisioned concurrency for our shape, but not by the 3x the marketing implied.
A rough take, not a benchmark: for spiky APIs that need consistent p99, Cloud Run came out 20 – 40% cheaper than Lambda with equivalent latency SLOs. For truly bursty, low-average workloads (think webhook receivers that idle 90% of the day), Lambda was cheaper because you pay literally nothing at idle.
The observability tax
This one gets forgotten. Lambda's default CloudWatch Logs pricing is generous until it isn't. At our peak, we were ingesting ~40 GB/day of Lambda logs, which cost more than the Lambda compute itself. Cloud Run logs into Cloud Logging with a similar trap — the free tier is 50 GB/project/month, and we blew through it in a week during load testing.
We now ship structured logs directly to a self-hosted collector via OpenTelemetry for both platforms, and only send WARN+ to the cloud provider's native logging. That change alone saved more than any compute optimisation.
When we'd pick which
After six months, our decision tree looks like this:
- Very spiky, per-request isolation matters, low average QPS: Lambda. The scale-to-zero economics are real if you don't need low p99 during spikes.
- Sustained traffic with spikes, latency-sensitive, needs concurrency: Cloud Run. The per-instance concurrency model is a huge unfair advantage here.
- Heavy VPC / RDS dependency: lean Cloud Run, or accept RDS Proxy on Lambda. The connection-per-execution model is a genuine liability.
- Team already deep in one ecosystem: stay. Neither platform is different enough to justify the migration tax unless you're bleeding on the current one.
Avoid the framing of "which is better". They're shaped differently. Lambda is a function runtime that pretends to be a service; Cloud Run is a container service that pretends to be serverless. That distinction predicts almost every tradeoff you'll hit.
Where we'd start
If you're evaluating this for a real workload, don't trust our numbers or anyone else's. Do this instead:
- Pick one endpoint that represents your ugliest traffic shape. Not the average — the ugly one.
- Deploy it to both platforms with realistic dependencies (VPC, DB pool, secrets). No hello-world.
- Run a two-week shadow test with a small traffic percentage, and watch p95, p99, and error rate during natural spikes, not synthetic ones.
- Add the logging and egress costs to your spreadsheet from day one. They're not a rounding error.
- Only then read the pricing calculators.
If you want help sizing this properly on your own workload, our DevOps and cloud team does this kind of comparison work regularly, and we're happy to share the load-test harness we used. It's not magic — it's just k6 with a nasty traffic profile — but it beats guessing.
Want a team like ours?
72Technologies builds production software for the kind of teams who actually read this blog.
Start a projectKeep reading

AWS NAT Gateway Bills Ate Our Margins. Here's How We Cut Them 78%.
A single misconfigured VPC route turned our NAT Gateway into a five-figure monthly line item. Here's the audit trail, the fixes, and what we'd do differently.

Vercel Preview Deployments Are Leaking Secrets. Audit Yours Now.
Preview URLs are treated like staging by developers and like production by attackers. Here's how we found real secrets exposed across three client accounts, and the guardrails we now enforce by default.

Sentry Performance Costs Doubled Overnight. Here's What We Found.
Our Sentry bill jumped from ~$900 to ~$2,100 in a single billing cycle with no traffic change. Here's the investigation, the culprits we found, and the sampling strategy we settled on.
