All articles
DevOps & CloudSeptember 5, 2026 6 min read

GCP Cloud Run vs AWS Lambda for a Bursty API: What We Actually Measured

We ran the same Node API on Cloud Run and Lambda for a client with spiky, unpredictable traffic. The winner wasn't obvious, and the reasons weren't the ones the marketing pages hint at.

GCP Cloud Run vs AWS Lambda for a Bursty API: What We Actually Measured

A client came to us last quarter with an unglamorous problem: their public API had a traffic pattern that looked like a heart monitor. Long flat stretches, then 40x spikes when a partner integration fired batch jobs. They were on AWS Lambda, paying for provisioned concurrency they mostly didn't use, and asked whether GCP Cloud Run would be cheaper and calmer. We ran both in production shadow mode for six weeks. Here's what actually happened.

The workload, honestly described

Before any comparison is useful, the shape of the traffic matters more than the platform. This was a Node 20 API doing three things: validating webhooks, enriching them against a Postgres read replica, and pushing normalized events onto a queue. Median request time was around 90ms. P99 was around 340ms, dominated by the DB round trip.

Traffic profile over a typical week:

  • Baseline: 5–15 requests/second
  • Partner batch windows (roughly 6 per day, 8–20 minutes each): 400–800 requests/second
  • Hard ceiling observed: ~1,100 requests/second during a Black Friday rehearsal

The existing Lambda setup used 512MB, an ALB in front, and 20 units of provisioned concurrency kept warm 24/7. That last part was the sore spot — provisioned concurrency was billed continuously but only earned its keep during bursts.

Why we didn't just tune Lambda harder

We considered it. Scheduled scaling on provisioned concurrency would help, but partner batch windows shifted, and the ops team didn't want a cron predicting them. Lambda SnapStart wasn't an option — it's Java and Python only at the time we tested (November 2025). So the question became: does a platform with per-instance concurrency handle this shape better?

Cold starts: the number everyone asks about first

We measured cold start latency by forcing scale-from-zero every 15 minutes for two weeks on both platforms. Same container image where possible (Cloud Run) and same zip artifact for Lambda, both around 42MB unpacked.

In our tests:

  • Lambda (Node 20, 512MB, zip): cold starts clustered around 380–520ms, with occasional outliers near 900ms when the VPC ENI attached.
  • Cloud Run (Node 20, container, 512MB, min instances = 0): cold starts clustered around 900ms–1.4s. Container pull dominated even with a slim base image.
  • Cloud Run with min instances = 1: cold starts effectively disappeared for baseline traffic but reappeared during scale-out bursts, in the 700–1,100ms range for each new instance.

Lambda won on raw cold start latency. That surprised no one. But cold start count is what actually hit the P99, and that's where the story flipped.

Cold starts per burst

Lambda scales by spinning one execution environment per concurrent request (concurrency is 1 per instance). A burst from 15 to 600 rps meant provisioning hundreds of new environments in seconds. Cloud Run defaults to a per-instance concurrency of 80, so the same burst needed roughly 8–12 new instances.

Over a two-week window with 84 partner batch events:

  • Lambda: an average of 340 cold starts per burst
  • Cloud Run (concurrency=80): an average of 11 cold starts per burst

Even with slower individual cold starts, Cloud Run's total cold-start-affected requests were roughly 30x fewer. P99 during bursts dropped from around 1.9s on Lambda (without provisioned concurrency headroom) to around 620ms on Cloud Run.

Concurrency model: the actual differentiator

This is the part the docs bury. Lambda's one-request-per-instance model is beautifully simple. You never think about in-process concurrency, connection pools are trivially sized (1), and blast radius per request is contained.

Cloud Run's per-instance concurrency is a knob that changes everything downstream:

# Cloud Run service YAML excerpt
spec:
  template:
    spec:
      containerConcurrency: 80
      timeoutSeconds: 30
      containers:
        - image: gcr.io/PROJECT/api:sha-abc123
          resources:
            limits:
              cpu: '1'
              memory: 512Mi

At concurrency 80, one instance handles up to 80 in-flight requests. That means:

  • Your Node event loop actually has to be non-blocking. Any synchronous JSON parse of a 5MB payload will stall 79 other requests.
  • Your DB pool needs sizing. We landed on 10 connections per instance after a painful afternoon of too many connections errors on the read replica.
  • Memory pressure is real. 80 concurrent requests each holding a 2MB enrichment context = 160MB, before Node overhead.

We eventually tuned to concurrency=40 with 1 vCPU and 1GB. Cost went up slightly, tail latency got noticeably calmer.

The connection pool trap

On Lambda, we ran RDS Proxy because 300 warm instances each opening their own Postgres connection would kill the DB. On Cloud Run at concurrency=40, we needed roughly 15 instances at peak, each with a pool of 10 — 150 connections total, well within Postgres limits. We removed a proxy layer entirely. That was worth real money and one fewer thing to page on.

Cost: not what the calculators say

Both platforms have calculators. Both lie by omission because they ignore the shape of your traffic and the surrounding infrastructure.

Our measured monthly costs for the same production traffic (~1.2 billion requests/month, mostly during bursts):

  • Lambda (with 20 units provisioned concurrency, ALB, RDS Proxy): approximately $2,850/month, of which about $780 was provisioned concurrency sitting idle and $190 was RDS Proxy.
  • Cloud Run (min instances = 2, no proxy): approximately $1,640/month.

Cloud Run was roughly 42% cheaper for this specific workload. The savings came from two places most comparisons miss: no idle provisioned concurrency, and dropping the connection proxy. Pure per-request pricing was actually closer to a wash — Lambda's request pricing is cheaper per-million, but Cloud Run's higher per-instance utilization made GB-seconds cheaper in aggregate.

If your traffic is flat, Lambda's per-request model likely wins. If it's spiky, Cloud Run's concurrency model earns its keep.

Operational rough edges

The things that made us mutter at our screens:

Cloud Run:

  • Deploys are slower. Container build + push + revision rollout was 3–5 minutes vs Lambda's 30–60 seconds for a zip update.
  • Traffic splitting between revisions is excellent, but the CLI ergonomics around it are clunky.
  • Cold start observability is thin. You can infer it from container/startup_latencies, but there's no first-class "this request hit a cold start" flag like Lambda's init duration.
  • Regional failover is manual. There's no native multi-region service; you build it with a global load balancer and two services.

Lambda:

  • Provisioned concurrency scheduling is a whole product on its own.
  • Log volumes in CloudWatch got expensive fast — we ended up shipping to a third-party sink.
  • The 6MB synchronous payload limit bit us once on a partner uploading base64 images we hadn't anticipated.
  • Version + alias juggling is powerful but easy to get wrong under pressure.

Observability

Both platforms integrate cleanly with OpenTelemetry now, but the paths differ. On Lambda we used the ADOT layer; on Cloud Run we ran the collector as a sidecar container. Sidecar cost us about 40MB of memory per instance but gave us richer control over sampling. If you're setting up tracing from scratch, we've written about the sampling tradeoffs we learned the hard way — worth reading before you commit to head-based sampling.

When we'd still pick Lambda

This wasn't a landslide, and Cloud Run isn't the answer for everything. We'd still reach for Lambda when:

  • Traffic is flat or predictable — the concurrency-model advantage disappears.
  • You need sub-100ms cold starts and can use SnapStart (Java/Python).
  • The workload is genuinely event-driven glue: S3 triggers, DynamoDB streams, EventBridge fan-out. The AWS-native integrations are still ahead.
  • Your team's operational muscle memory is entirely AWS. Retraining costs are real.

And we'd pick Cloud Run when:

  • Traffic is bursty and you're already paying for provisioned concurrency you resent.
  • Your request handlers are I/O-bound and safe to run concurrently in-process.
  • You want to ship the same container to staging, prod, and a developer's laptop without adaptation.

Where we'd start

If you're staring at a Lambda bill with a fat provisioned-concurrency line item, don't rewrite anything yet. Do this instead: containerize your handler behind a thin HTTP wrapper (Fastify or Hono work well), deploy it to Cloud Run in shadow mode for two weeks, and mirror 10% of traffic. Compare P99 during your actual bursts, not synthetic load tests. Measure the connection-pool footprint on your database — that's often where the real savings or the real surprises live.

If you want a second pair of eyes on a migration like this, our cloud and DevOps team does exactly this kind of workload profiling. But honestly, most teams have the data already. It's sitting in CloudWatch, waiting for someone to ask the right question.

#AWS#GCP#Serverless#Cloud Run#Lambda#DevOps

Want a team like ours?

72Technologies builds production software for the kind of teams who actually read this blog.

Start a project