Cloud

Cloud Run gave us Kubernetes-like scaling without running a cluster

CLD

We have been cautious about recommending Kubernetes to clients whose traffic and service count do not really justify the operational overhead, but those same clients often still want the deployment consistency that containers give us. Google Cloud Run, which reached general availability last year and matured a lot through this year, has become our answer for exactly that gap.

What it gives us

  • Any container that listens on a port can be deployed, with no Kubernetes manifests or cluster to manage
  • Automatic scaling from zero to many instances based on incoming traffic, and back down to zero — and to nothing — when there is none
  • A genuinely simple deploy command from our existing Docker-based CI pipeline, with almost no new tooling to learn

Where it falls short

  • Cold starts on scale-from-zero are a real consideration for latency-sensitive endpoints
  • Long-lived connections like WebSockets need careful handling, since Cloud Run's request model assumes relatively short-lived HTTP requests
  • Less configuration control than a full Kubernetes cluster gives you, which is the whole point but occasionally a real limitation

For several smaller and mid-sized client projects this year, Cloud Run has replaced what would previously have been either a Kubernetes cluster that was overkill or a fleet of manually managed EC2 instances. It is now our default recommendation for containerized workloads that do not have Kubernetes-specific requirements.

A concrete migration: a client's API and background worker

One mid-sized client's setup, previously three EC2 instances running a Node API, a background worker, and a small internal admin tool, moved to three separate Cloud Run services this year. Each service now scales independently based on its own traffic, which matters because the admin tool sees essentially no traffic outside business hours and previously ran on a dedicated instance purely out of convenience, quietly costing the client money for capacity nobody was using most of the day. Under Cloud Run, that same admin tool now scales to zero overnight and on weekends, and the client's total compute bill for these three services dropped by more than half compared to the always-on EC2 setup it replaced.

Working around cold starts for the endpoints that need it

For the one endpoint on this client's API where cold-start latency genuinely mattered — a customer-facing search endpoint — we configured a minimum instance count of one rather than allowing it to scale all the way to zero, trading a small amount of the cost savings for consistently fast response times on that specific path. Cloud Run's per-service configuration made this an easy, isolated decision rather than an all-or-nothing tradeoff across the whole application, which is exactly the kind of fine-grained control that would have been harder to reason about on a single shared Kubernetes deployment with mixed traffic patterns.

Observability differences worth knowing about upfront

Cloud Run's built-in logging and request tracing integrate directly with Google Cloud's operations suite with effectively no setup, which is a real advantage over a self-managed Kubernetes cluster where we would typically need to deploy and maintain our own logging and metrics stack. The tradeoff is that some of the deeper introspection tools we are used to on Kubernetes — being able to shell into a running pod to debug a live issue, for instance — simply are not available on Cloud Run's more locked-down container model, which pushed us toward more disciplined structured logging and health-check endpoints from the start rather than leaning on live debugging access as a fallback.

Handling database connections from a scale-to-zero service

A service that scales to zero and then cold-starts under new traffic needs to open fresh database connections each time, and doing this naively — a new connection pool created per cold start with no upper bound — caused one client's Cloud SQL instance to briefly hit its connection limit during a traffic spike that triggered many simultaneous cold starts. We fixed this with Cloud SQL's connection pooling proxy and a conservative per-instance connection limit, a configuration detail that is easy to miss when a service's traffic pattern during development never exercises the many-simultaneous-cold-starts scenario that eventually surfaced it in production.

Where we still reach for Kubernetes instead

None of this makes Kubernetes obsolete for the clients who genuinely need it. A client running a dozen or more interdependent services with complex networking requirements, or one needing fine-grained control over resource scheduling across a shared cluster, still gets more value from Kubernetes than Cloud Run's simpler, more opinionated model can offer. Our rule of thumb this year has settled into recommending Cloud Run by default and only reaching for a full Kubernetes cluster once a client's actual requirements — not just their expected future scale — genuinely call for the additional control.

CI/CD pipeline changes this required

Moving from EC2 deploys to Cloud Run meant reworking our CI pipeline's final deploy step, but far less than we expected — since we were already building Docker images for these services, the main change was swapping an SSH-and-restart deploy script for a single `gcloud run deploy` command pointed at the newly built image. Rollback also became simpler: Cloud Run keeps previous revisions available and lets us shift traffic back to an earlier one instantly if a new deploy misbehaves, which previously would have meant redeploying an older build to EC2 and waiting through the same deploy process again under worse, more stressful conditions.

What this changed about how we price and scope smaller projects

Knowing we can quote a client a genuinely usage-based infrastructure cost, rather than a fixed monthly server cost sized for their expected peak traffic, has changed some of our project scoping conversations. A client with genuinely spiky or seasonal traffic — a retailer with a holiday rush, for instance — can now be quoted honestly around Cloud Run's scale-to-zero pricing model instead of us either over-provisioning a fixed server for a peak that only lasts a few weeks a year, or under-provisioning it and risking a slow site during exactly the period that matters most to the client's business.

Results so far

For several smaller and mid-sized client projects this year, Cloud Run has replaced what would previously have been either a Kubernetes cluster that was overkill or a fleet of manually managed EC2 instances. It is now our default recommendation for containerized workloads that do not have Kubernetes-specific requirements, and the operational time we used to spend patching and right-sizing EC2 instances for these smaller clients has mostly disappeared.

Regional deployment considerations for a client with global traffic

One client's user base skews heavily toward Europe despite the company itself being based in North America, which pushed us to deploy that client's Cloud Run services in a European region rather than defaulting to the same region we use for most of our other GCP work. Cloud Run makes multi-region deployment straightforward in principle, but keeping request latency low for the client's dominant user base while keeping operational complexity low meant deliberately choosing a single well-placed region rather than over-engineering a multi-region active-active setup this particular client's traffic did not yet justify.

How this changed our incident response runbooks

Cloud Run's revision-based deployment model changed one specific part of our incident response process for the better: rolling back a bad deploy is now a traffic-shifting operation against existing, already-running revisions rather than a fresh deploy of a previously known-good build, which is meaningfully faster during an actual incident when every minute matters. We updated our incident runbooks to reflect this, replacing a "redeploy the previous git tag" step with a "shift traffic back to the prior revision" step, which our on-call engineers have already used once, during a real if minor incident, faster than the old EC2-based rollback process would have allowed.

A cost-monitoring habit this pushed us to adopt

We also started reviewing each Cloud Run service's actual usage-based billing monthly rather than only when a client asks, since the pay-per-use model means a genuinely idle service costs almost nothing but a service quietly handling more traffic than expected shows up immediately as a real cost signal worth investigating, in a way a fixed-price EC2 instance's flat monthly bill never would have surfaced on its own. We plan to review this same cost-monitoring habit against every other GCP service we manage for clients, not just Cloud Run, since the underlying discipline of checking actual usage-based cost monthly rather than reactively is one worth generalizing well beyond this one product.

← Back to the journal

Have a project in mind?
Let’s talk.

Tell us where you are and where you want to go. We'll map the fastest route between the two.

Currently accepting new clients