DEV Community

Honey Kiran
Honey Kiran

Posted on Originally published at honeykiran.substack.com on

Cloud Run Is Reliable. Your Defaults Are the Risk.

Cloud Run is reliable. Your defaults are the risk. A request flows to a Cloud Run service and its instances, warm or cold, under a 99.95 percent SLA badge — with a dashed cold-start path underneath that nobody budgeted for.

Over on r/googlecloud, in a thread titled “How reliable is Cloud Run, really?”, an engineer was doing the homework before a serious move: a mission-critical, latency-sensitive service, currently on Heroku, possibly moving to Cloud Run — and one question underneath it all. Can I trust this thing?

It is the right question. It is also, forgive me, slightly the wrong question. Cloud Run (Google’s serverless container platform — you give it a container, it runs it, scales it, and bills you for the work) is reliable in the ways Google controls. The part that will actually decide your first month is the part Google does not control: the settings you leave at their defaults because the deploy worked on the first try and everyone went home happy.

Picture it — illustrative scene, but you have seen this film. The migration goes beautifully. Then the first quiet morning arrives, traffic dips, the platform politely scales everything to zero to save you money, and the first real user of the day pays for it in one slow request. Nobody’s dashboard turns red. The average still looks lovely. Only the slowest users know. They always know.

So let us read the fine print together. I did the reading so you do not have to lose your afternoon to twelve documentation tabs.

First, what the SLA actually promises

SLA means Service Level Agreement — the written promise, with money behind it. For Cloud Run services, Google’s SLA promises a monthly uptime percentage of 99.95% for a normal, non-GPU service in most regions. Stockholm is the odd one out at 99.9%, and a GPU service without zonal redundancy (extra copies spread across zones, so one zone failing does not take you down) sits at 99.5%. Fine numbers. Now read what counts as “down”.

Downtime, in this document, means an error rate above one percent — and only counting requests that fail with a 5xx error (a server-side failure) because of Cloud Run’s own infrastructure, with a minimum of 100 valid requests in the measurement window. If Google misses, the remedy is a financial credit against future bills — 10%, 25%, or 50% depending on how far it fell — and you have to ask for it within 30 days, with logs.

Notice two things, because they matter more than the headline number. One: errors caused by your own software are excluded. If your container crashes, that is your downtime, not Google’s, and the SLA stays silent. Two: the SLA counts errors, not slowness. A request that takes nine painful seconds and eventually returns a success counts as uptime. Your user experienced a failure. The contract experienced a triumph. Both are telling the truth, which is exactly why “is Cloud Run reliable?” needs a better follow-up: reliable at what, measured how, and whose settings?

The SLA promises the platform will answer. It never promised your defaults would let it answer fast.

Cold starts: the platform’s politest trap

A cold start is the delay when a request arrives and no running instance (a live copy of your container) exists to take it, so the platform has to start one first — pull things together, boot your process, run your startup code — while the request waits. By default, a Cloud Run revision (one immutable version of your service; every deploy creates a new one) with no traffic scales to zero instances. Zero is beautiful on the bill and brutal on the first request after a lull.

The fix everybody reaches for is minimum instances: you tell Cloud Run to keep a set number of instances warm and ready even when nothing is arriving. Set it to 1 and the quiet-morning problem mostly disappears for a small service. But be clear about the trade, because there are two catches hiding in it.

Catch one: warm is not free. Instances kept running by minimum instances do incur billing costs — a lower idle rate if you are on request-based billing, the full rate if you are on instance-based billing. You are renting a taxi that waits outside your house all night. Very convenient. Not charity.

Catch two: it is a best-effort target, not a guarantee — Google’s own words. Zone capacity trouble, infrastructure rebalancing, a container that crashes on startup, or a quota wall can all leave you temporarily below your floor. So minimum instances reduce cold starts; they do not delete the concept. Design as if a cold start can still happen occasionally — because it can — and make your startup path lean: small image, lazy loading of heavy things you do not need on every request, and startup CPU boost turned on (the --cpu-boost flag, which gives an instance extra CPU while it starts). Then measure your own cold start in staging. Do not copy anyone else’s number off the internet, including mine. There is no honest universal number; runtimes and images differ too much.

Architecture diagram: a request reaches the Cloud Run service, which routes by traffic split to either a warm instance of Revision A or down the cold-start path when no warm instance exists, then into your container up to its concurrency limit. The traffic split band shows 95 percent to Revision A and 5 percent to Revision B, with rollback as sending 100 percent back to A.

Concurrency: 80 strangers in one small room

Concurrency means how many requests one instance handles at the same time. This is where Cloud Run quietly differs from the old one-request-at-a-time mental model. The console default is 80 concurrent requests per instance, and you can raise it to a maximum of 1,000. If you deploy with the command line or Terraform, a brand-new service defaults to 80 times the number of vCPUs (the virtual CPU cores you gave the container) — so for a 1-vCPU service, that is also 80. Either way, picture it: 80 requests sharing one container, one CPU allowance, one memory allowance, and whatever database connections your code opens.

For a light, I/O-bound service (one that mostly waits on other systems — a quick database read, an API call, then done), 80 can be perfectly sensible and wonderfully cheap: fewer instances, less bill. For a CPU-heavy service (each request actually crunches — image work, heavy JSON, crypto, reports), 80 is a queue disguised as a setting. Requests do not fail. They wait politely behind each other inside your container, and your latency tail — the slowest few percent of requests, the p95 and p99 your heaviest users live in — grows a long, ugly beard.

One more nuance: the number is a ceiling, not a promise. If an instance’s CPU is already highly used, Cloud Run may send it fewer requests and scale out instead. And if your code cannot handle parallel requests safely — shared global state, not thread-safe — the docs’ advice is blunt: set concurrency to 1. Correct beats fast.

Here is a sane starting deploy for a latency-sensitive service — note that every interesting choice is explicit, nothing is left to vibes:

gcloud run deploy my-service \
  --image us-docker.pkg.dev/PROJECT_ID/REPO/my-service:TAG \
  --region us-central1 \
  --min-instances 1 \
  --max-instances 20 \
  --concurrency 40 \
  --cpu 1 \
  --memory 512Mi \
  --timeout 60s \
  --cpu-boost
Enter fullscreen mode Exit fullscreen mode

Read it like a contract: one warm instance always ready, at most twenty instances so a spike cannot stampede the database, forty concurrent requests per instance because we measured instead of guessing, and a 60-second timeout matched to the real work. Your numbers will differ. The habit must not.

Before and after, illustrative latency shape only — not measured data. On all defaults the typical request is fine but the tail, cold and queued, stretches far. Tuned on purpose — minimum one instance, measured concurrency, startup boost — the typical request is the same and the tail is far shorter.

CPU only exists while a request is looking at it

This is the default that surprises the most migrants, so sit with it for a second. Cloud Run has two billing settings. The default is request-based billing — the setting previously called “CPU only allocated during request processing”. Exactly what it sounds like: an instance gets CPU while it is processing a request, and is charged when it processes requests, starts, and shuts down. Between requests, your code effectively sits in a dark room.

The alternative is instance-based billing — previously “CPU always allocated”, switched on with --no-cpu-throttling — where CPU is allocated for the entire life of the instance and you are charged for that entire life. More money. Also, sometimes, the only correct answer.

Why? Because of everything your service does after replying. Flush the last log lines, finish the metric, close out the background task — under request-based billing, all of that crawls or stalls the moment the response leaves, because the CPU went home. Like a chef who may only move while a customer is watching: the dish got served; the kitchen never got cleaned. If your service does real background work, choose instance-based billing on purpose — or move that work to a proper queue. Do not discover the dark room in production.

Timeouts: the 504 that leaves the body behind

Every request gets a timeout: how long the service may take before Cloud Run gives up on it. The default is 5 minutes (300 seconds), and you can extend it up to a maximum of 60 minutes (3,600 seconds). For most migrated web services this is plenty of headroom — which is precisely why people stop thinking about it, right up until a client, a load balancer, or a framework setting somewhere else in the chain gives up much earlier and everyone debugs the wrong layer.

And here is the detail worth memorising: on timeout, Cloud Run closes the connection and returns an HTTP 504 — but it does not terminate the container. Your code may keep processing the dead request, burning CPU your live requests needed. So set the timeout just above your honest worst case, bail out early inside long handlers, and if a job regularly wants near an hour, it is asking to be a background job, not a web request.

Revisions: the part Cloud Run genuinely does beautifully

Now the good news, and it is genuinely good. Every change — new image, new setting — creates a new revision, and Cloud Run lets you choose which revisions get traffic and in what percentage. Deploy the new revision with no traffic, look at it directly, then send it 5%. Watch it. Then 50%, then all of it. If it misbehaves at any point, rollback is not a redeploy and a prayer; it is one traffic command sending 100% back to the previous revision:

# deploy the new revision, but give it no traffic yet
gcloud run deploy my-service \
  --image us-docker.pkg.dev/PROJECT_ID/REPO/my-service:TAG \
  --region us-central1 \
  --no-traffic --tag canary

# let the canary revision taste 5 percent of real traffic
gcloud run services update-traffic my-service \
  --region us-central1 \
  --to-tags canary=5

# rollback: send everything back to the previous revision
gcloud run services update-traffic my-service \
  --region us-central1 \
  --to-revisions PREVIOUS_REVISION=100
Enter fullscreen mode Exit fullscreen mode

Two honest caveats. Traffic changes are not instantaneous, and requests already in flight are not dropped during a switch — they finish on whichever revision took them. That is good behaviour, but it means “we rolled back” and “the bad revision stopped mattering” are a few moments apart, not the same instant. And a rollback only rescues you if the previous revision is actually still good and your data changes were backwards-compatible. A new revision writing a new database shape that the old revision cannot read will make your beautiful one-command rollback into a one-command second outage. Rehearse the rollback in staging, with the database change included, before you need it at speed.

Staging warning: test every one of these settings in a staging service first — minimum instances, concurrency, timeout, traffic split, and the rollback command itself — with the same defaults you plan for production. A rollback you have never run is a rumour, not a plan.

The ten pre-migration reliability questions as a checklist visual: minimum instances, concurrency fit, CPU mode on purpose, timeout fit, maximum instances your database can survive, gradual rollout, a tested rollback, what the SLA counts and excludes, background work finished inside the request, and a load test in staging first.

The pre-migration reliability checklist

Pin this. Ten questions, answered in staging, before Heroku becomes a memory:

  1. Is minimum instances set — or is cold the plan? If the service is latency-sensitive, a warm floor (often 1 to start) is a decision, not an accident. Know its idle cost and its best-effort limits.

  2. Does concurrency match your code? Load-test at your chosen value. CPU-heavy means lower; light and I/O-bound can go higher; unsafe in parallel means 1.

  3. CPU mode: chosen, or inherited? Request-based by default. If anything must run outside a request — background flushes, threads, agents — switch to instance-based billing deliberately.

  4. Is the timeout longer than your real work — and not longer? Default 5 minutes, maximum 60. Add headroom, add early bail-out, and move hour-long work out of requests entirely.

  5. Can your database survive your maximum instances? Instances times connections per instance is a number. Compute it before a traffic spike computes it for you, then cap max instances to fit.

  6. Does every deploy land with no traffic first? New revision dark, checked directly, then 5%, then watched at each step up. No big-bang cutovers.

  7. Has the rollback command been run, not just written? One command back to the previous revision — rehearsed, with the data change in the rehearsal.

  8. Do you know what the SLA excludes? Your code’s errors are yours. Slow-but-successful is “uptime”. Plan your own error budget on top of the 99.95%.

  9. Is all background work finished inside the request? If not, it needs instance-based billing or a queue. Test what happens after the response is sent, not just before.

  10. Did the load test happen in staging first? Cold starts, the latency tail, and the rollback — all three observed in staging, none of them debuted in production.

Trust the platform. Audit the defaults. So, back to that thread’s question: how reliable is Cloud Run, really? Reliable — measurably, contractually, 99.95%-with-credits reliable — at the layer Google runs. Your service’s reliability is a separate number, written almost entirely in six settings most teams never open: minimum instances, concurrency, CPU allocation, timeout, maximum instances, and how traffic moves between revisions.

None of this is an argument against migrating. Cloud Run honours the platform bargain — with sharper tools and sharper edges. Keep the bargain. Just read the settings page like it is part of the contract, because for your users, it is the only part they will ever feel.


© 2026 Honey Kiran. All rights reserved.

Honey Kiran · DevOps & Platform Engineering

Cloud, Kubernetes, DevOps, SRE, and platform engineering — explained the practical way.

Moving something latency-sensitive to Cloud Run — or already there and wondering which default is quietly taxing you? Tell me which setting bit you first in the comments.

Examples are illustrative — validate in your own environment before running in production.

Disclaimer

  • This post describes my personal experience with my own systems and accounts, in my own words. It is a first-person account of what I observed and what I do in my own work — not a statement of any company’s intentions, policies, or internal decisions. Where I do not know something, the post says so.

  • I am not affiliated with, endorsed by, or speaking for any company, product, or vendor named in this post. All product and company names are trademarks of their respective owners and are mentioned only to describe the experience accurately.

  • Any error messages, documentation excerpts, or figures quoted here are reproduced from my own records or from the public sources linked in the post, believed accurate at the time of writing. Product behavior changes over time; verify against current official documentation before relying on any detail.

  • Nothing in this post is professional, financial, legal, or security advice for your specific situation. Examples are illustrative — validate in your own environment before running anything in production.

  • Corrections are welcome. If any company named here publishes an official statement, or if any detail is wrong, I will update this post and note the correction. The goal is an accurate public record, not a verdict.

Top comments (0)