Cloud Run min instances: idle is $9.72 or $49.25

Published · Updated · OhChimp Engineering

An idle Cloud Run container held warm by a minimum instance setting, billing every second it serves no requests

A Cloud Run service with a minimum instance count keeps a container warm around the clock, and you pay for every second of that. In us-central1 an idle instance at 1 vCPU and 512 MiB costs about $9.72 a month under request-based billing, and about $49.25 under instance-based billing. The container is the same and the traffic is zero either way, so the billing mode decides which number you pay.

Why a warm instance bills when nobody calls it

Cloud Run scales to zero by default. No requests means no instances, and no instances means no compute charge. Setting a minimum instance count opts out of that. Google's minimum instances documentation says Cloud Run keeps at least that many instances running even when they are not processing requests, and that those instances do incur costs.

What those seconds cost depends on a setting most teams pick once and never revisit. Cloud Run has two billing modes. Request-based billing, the default, charges CPU only while an instance is starting or handling a request. Instance-based billing charges for the entire lifetime of the instance, from start to shutdown, whether requests arrive or not.

Here are the published us-central1 rates for both.

Meter, us-central1 Request-based billing Instance-based billing
CPU while serving a request $0.000024 per vCPU-second $0.000018 per vCPU-second
CPU while idle on a min instance $0.0000025 per vCPU-second $0.000018 per vCPU-second
Memory $0.0000025 per GiB-second $0.000002 per GiB-second
Requests $0.40 per million no per-request charge

Request-based billing charges idle CPU at $0.0000025 per vCPU-second, against $0.000024 while a request is in flight, so the seconds a warm instance spends waiting cost 9.6 times less than the seconds it spends working. Instance-based billing charges $0.000018 per vCPU-second for every second the instance exists, serving or idle.

A 30-day month is 2,592,000 seconds, so one instance held at 1 vCPU and 512 MiB with no traffic at all costs:

Moving one idle 1 vCPU, 512 MiB instance from request-based to instance-based billing therefore adds $39.53 a month for a container that served nothing, decided by one flag. Ten services parked at one warm instance each is roughly $395 a month of difference.

Those are gross figures. The monthly free tier under request-based billing covers 180,000 vCPU-seconds and 360,000 GiB-seconds, which is about 7% of a single always-on instance-month's CPU but about 28% of its memory, and it is shared across everything in the billing account.

The idle discount exists in only one billing mode

The reduced idle rate of $0.0000025 per vCPU-second belongs to request-based billing alone. Instance-based billing has no idle rate at all, so every second bills at the full $0.000018 per vCPU-second whether a request is in flight or not.

Under request-based billing, memory bills at $0.0000025 per GiB-second whether the instance is serving or sitting still. The idle discount touches CPU only, so a memory-heavy service saves proportionally less by staying on the default mode.

Instance-based billing exists because CPU stays allocated outside the request, which is what lets a service finish background work after it returns a response. It is also cheaper per busy second: $0.000018 against $0.000024 is 25% off the CPU you actually use, and it drops the per-request charge entirely.

Work the crossover in units of instance-lifetime seconds, where u is the fraction of that lifetime spent processing requests:

instance-based:  $0.000018
request-based:   u x $0.000024 + (1 - u) x $0.0000025

0.000018 = 0.0000025 + u x 0.0000215
u = 0.0000155 / 0.0000215 = 72.1%

CPU costs cross over at about 72% of instance lifetime spent processing requests. Request-based billing is cheaper below that line, and instance-based billing is cheaper above it. Counting the cheaper instance-based memory rate as well, the whole-instance crossover for a 1 vCPU, 512 MiB service falls to about 70.9% busy, and heavy request volume pushes it lower still, because instance-based billing has no $0.40 per million to pay.

A service pinned at one warm instance to kill cold starts is nowhere near 70% busy, because if it were busy it would have instances anyway.

The metric with "billable" in its name leaves the idle time out

The obvious place to check your exposure is run.googleapis.com/container/billable_instance_time. For a given container instance, Google's metrics reference defines it as the time when the instance is starting or when at least one request is being processed, rounded up to the nearest 100 milliseconds.

Idle seconds on a warm minimum instance meet neither condition, so they never enter that count. The pricing page bills them anyway, at the idle rate for whichever mode the service runs. A Cloud Run service parked at a minimum instance count with no traffic can therefore show a near-flat billable_instance_time graph while billing every second of the month.

Two other metrics are denominated in the same units the price list uses, which makes them the ones to reach for:

Multiply those by the rates above and you get money rather than a ratio. To split warm from working, run.googleapis.com/container/instance_count carries a state label whose value is either active or idle.

Find your warm and idle services by hand

Two annotations on the revision template hold what you need: whether a service has a floor set, and which billing mode it runs. Read them as JSON rather than guessing at a format string.

REGION=us-central1

for svc in $(gcloud run services list --region="$REGION" --format='value(metadata.name)'); do
  gcloud run services describe "$svc" --region="$REGION" --format=json
done | jq -r '
  (.spec.template.metadata.annotations // {}) as $a
  | [ .metadata.name,
      ($a["autoscaling.knative.dev/minScale"] // "0"),
      (if $a["run.googleapis.com/cpu-throttling"] == "false"
       then "instance-based" else "request-based" end)
    ] | @tsv'

Anything with a minScale above zero is paying a floor, and the ones reading instance-based are paying that floor at the full CPU rate.

Cloud Run is regional, and a service parked in a region nobody opens is the one that survives longest. Sweep them all:

for REGION in $(gcloud run regions list --format='value(locationId)'); do
  gcloud run services list --region="$REGION" --format='value(metadata.name)' 2>/dev/null \
    | sed "s|^|$REGION |"
done

Now ask how much traffic those services actually took. There is no gcloud monitoring time-series command, so query the Monitoring API directly.

PROJECT=$(gcloud config get-value project)
TOKEN=$(gcloud auth print-access-token)
END=$(date -u +%Y-%m-%dT%H:%M:%SZ)
START=$(date -u -v-30d +%Y-%m-%dT%H:%M:%SZ)   # GNU date: date -u -d '30 days ago' +%Y-%m-%dT%H:%M:%SZ

curl -s -G "https://monitoring.googleapis.com/v3/projects/$PROJECT/timeSeries" \
  -H "Authorization: Bearer $TOKEN" \
  --data-urlencode 'filter=metric.type="run.googleapis.com/request_count"' \
  --data-urlencode "interval.startTime=$START" \
  --data-urlencode "interval.endTime=$END" \
  --data-urlencode 'aggregation.alignmentPeriod=2592000s' \
  --data-urlencode 'aggregation.perSeriesAligner=ALIGN_SUM' \
  --data-urlencode 'aggregation.crossSeriesReducer=REDUCE_SUM' \
  --data-urlencode 'aggregation.groupByFields=resource.labels.service_name' \
| jq -r '.timeSeries[]? | [.resource.labels.service_name,
                           (.points[0].value.int64Value // "0")] | @tsv'

A service on that min-instance list with a request count of zero, or a few hundred health checks, is the shape you are looking for. crossSeriesReducer requires perSeriesAligner and an alignment period, so all three of those parameters have to travel together or the API returns an error.

Then price the floor from the metric that is already in billing units:

curl -s -G "https://monitoring.googleapis.com/v3/projects/$PROJECT/timeSeries" \
  -H "Authorization: Bearer $TOKEN" \
  --data-urlencode 'filter=metric.type="run.googleapis.com/container/cpu/allocation_time"' \
  --data-urlencode "interval.startTime=$START" \
  --data-urlencode "interval.endTime=$END" \
  --data-urlencode 'aggregation.alignmentPeriod=2592000s' \
  --data-urlencode 'aggregation.perSeriesAligner=ALIGN_SUM' \
  --data-urlencode 'aggregation.crossSeriesReducer=REDUCE_SUM' \
  --data-urlencode 'aggregation.groupByFields=resource.labels.service_name' \
| jq -r '.timeSeries[]? | [.resource.labels.service_name,
                           (.points[0].value.doubleValue // 0)] | @tsv'

Multiply the vCPU-seconds by $0.000018 for an instance-based service, or by $0.0000025 for the idle share of a request-based one. To see how much of that allocation was warm rather than working, pull container/instance_count split by state:

curl -s -G "https://monitoring.googleapis.com/v3/projects/$PROJECT/timeSeries" \
  -H "Authorization: Bearer $TOKEN" \
  --data-urlencode 'filter=metric.type="run.googleapis.com/container/instance_count" AND metric.labels.state="idle"' \
  --data-urlencode "interval.startTime=$START" \
  --data-urlencode "interval.endTime=$END" \
  --data-urlencode 'aggregation.alignmentPeriod=3600s' \
  --data-urlencode 'aggregation.perSeriesAligner=ALIGN_MEAN' \
  --data-urlencode 'aggregation.crossSeriesReducer=REDUCE_MEAN' \
  --data-urlencode 'aggregation.groupByFields=resource.labels.service_name'

Drop the floor without buying cold starts back

Three changes are available, and they are not equally safe.

Switching billing mode is the cheapest one to try, because it changes no capacity. If a service sits well under the crossover, put it back on request-based billing:

gcloud run services update SERVICE \
  --region=us-central1 \
  --cpu-throttling

That takes the idle floor on a 1 vCPU, 512 MiB service from about $49.25 a month to about $9.72. Confirm first that nothing in the container depends on CPU outside a request. Background writes kicked off after the response, queue draining, and cache warmers all stop getting CPU when throttling comes back on.

Lowering the floor is next. A service holding three warm instances for a traffic shape that only ever needed one is paying triple for the privilege:

gcloud run services update SERVICE \
  --region=us-central1 \
  --min-instances=1

Removing the floor entirely is the biggest saving and the one that changes user-visible behaviour, because cold starts come back:

gcloud run services update SERVICE \
  --region=us-central1 \
  --min-instances=0

Do that on internal tools, staging services, and anything asynchronous before you touch a user-facing endpoint. Check request_latency/pending afterwards, which measures the wait from a request reaching Cloud Run to it reaching an instance. That is where a returning cold start shows up.

If a user-facing service has to lose its floor, startup CPU boost shortens the cold starts that come back. Google's CPU limits documentation says it gives an instance extra CPU while it starts and for 10 seconds afterwards, 2 vCPUs for a service limited to 1, and bills the boosted CPU only for that startup window:

gcloud run services update SERVICE \
  --region=us-central1 \
  --cpu-boost

At the request-based rate, 2 vCPUs for 20 seconds is about a tenth of a cent per cold start, so turn the boost on and re-check request_latency/pending before deciding an endpoint still needs a warm instance.

Rolling back

Every one of these lands as a new revision, so rollback is a routing decision rather than a reconstruction. Find the previous revision and send traffic back to it:

gcloud run revisions list --service=SERVICE --region=us-central1

gcloud run services update-traffic SERVICE \
  --region=us-central1 \
  --to-revisions=PREVIOUS_REVISION=100

Or reverse the setting directly, which is the cleaner option when you want the newest image plus the old configuration:

gcloud run services update SERVICE --region=us-central1 --min-instances=2
gcloud run services update SERVICE --region=us-central1 --no-cpu-throttling

If you manage services declaratively, both settings are annotations, so the same change belongs in the YAML you apply with gcloud run services replace:

spec:
  template:
    metadata:
      annotations:
        autoscaling.knative.dev/minScale: "1"
        run.googleapis.com/cpu-throttling: "true"

If you change it in the console and leave the YAML alone, your next deploy puts the bill back.

A quick decision tree

graph TD
  A[Cloud Run service with min instances above zero] --> B{Any real request traffic in 30 days?}
  B -- No --> C[Set min-instances=0 and let it scale to zero]
  B -- Yes --> D{Does it need CPU outside a request?}
  D -- No --> E{Busy more than about 70% of instance lifetime?}
  E -- No --> F[Use request-based billing: idle CPU drops to a tenth]
  E -- Yes --> G[Keep instance-based billing: 25% off the CPU you use]
  D -- Yes --> H[Keep instance-based billing and cut the floor instead]
  H --> I{Floor higher than peak concurrency needs?}
  I -- Yes --> J[Lower min-instances one step and watch pending latency]
  I -- No --> K[Right-size CPU and memory on the revision]

Paying for reserved capacity rather than work done is the same bill wearing different clothes.

Common questions

Do Cloud Run minimum instances cost money when no requests arrive?

Yes. Cloud Run keeps the minimum number of instances running whether or not requests arrive, and those instances bill. Under request-based billing the idle seconds are charged at a reduced CPU rate of $0.0000025 per vCPU-second in us-central1, against $0.000024 while serving. Under instance-based billing there is no reduced rate, so idle seconds bill at the full $0.000018 per vCPU-second.

How much does one idle Cloud Run instance cost per month?

For 1 vCPU and 512 MiB in us-central1 across a 30-day month, about $9.72 under request-based billing and about $49.25 under instance-based billing. Doubling CPU roughly doubles both figures, since CPU is the larger half in each case. Both numbers are gross of the monthly free tier.

What is the difference between request-based and instance-based billing?

Request-based billing, the default, charges CPU only while an instance starts up or processes a request, and adds $0.40 per million requests. Instance-based billing charges for the whole instance lifetime at a lower CPU rate of $0.000018 per vCPU-second with no per-request charge, and it keeps CPU allocated between requests so background work can run after the response.

When is instance-based billing actually cheaper?

When an instance spends more than about 72% of its lifetime processing requests. That is where the flat $0.000018 per vCPU-second crosses the request-based blend of $0.000024 active and $0.0000025 idle. Cheaper instance-based memory brings the whole-instance crossover for a 1 vCPU, 512 MiB service to roughly 70.9%, and heavy request volume lowers it further because instance-based billing skips the per-request fee.

How do I switch a Cloud Run service back to request-based billing?

Run gcloud run services update SERVICE --region=REGION --cpu-throttling. The reverse is --no-cpu-throttling. In YAML it is the run.googleapis.com/cpu-throttling annotation on the revision template, set to "true" for request-based. Check that nothing in the container relies on CPU after the response returns, because throttling stops it.

Why does billable instance time look low on a service that is costing me money?

Because container/billable_instance_time counts time when an instance is starting or processing at least one request, and idle time on a warm minimum instance is neither, though the seconds bill anyway. Use container/cpu/allocation_time and container/memory/allocation_time instead, which are reported in vCPU-seconds and GiB-seconds, the same units the price list is written in.

Will removing minimum instances bring back cold starts?

Yes, and that is the trade you are making. Measure it rather than guessing: request_latency/pending records the wait between a request reaching Cloud Run and reaching an instance, which is where a cold start lands. Startup CPU boost (--cpu-boost) shortens each one at the cost of extra CPU for the startup window only. Take the floor off internal and asynchronous services first, then decide whether a user-facing endpoint is worth $9.72 a month to keep warm.

Do these rates change by region or with a committed use discount?

Both move them. Every figure here is the default on-demand rate, and Google's free-tier note for these tables is written against us-central1, so check your own region before committing to a number. A one-year Cloud Run committed use discount takes request-based active CPU from $0.000024 to $0.00001992 per vCPU-second and idle CPU from $0.0000025 to $0.000002075, with instance-based CPU dropping from $0.000018 to $0.00001494. That discount lands on both sides of the comparison, so the crossover stays at about 72%. A three-year Compute Flexible CUD discounts instance-based CPU harder and pulls the crossover down to roughly 43%.

How OhChimp approaches this

A warm instance bills the same amount every hour it exists, so it never arrives as a spike. It is a flat line nobody investigates, on a service somebody pinned during an incident two quarters ago.

OhChimp reads your Cloud Run services across every region and matches each one's minimum instance count and billing mode against the traffic it actually served, then prices the idle floor at your region's published rates. What comes back is a plan you read first, naming the change, the dollars a month it returns, and the command that puts it back. It sits there until you click apply. OhChimp cannot yet measure what one Cloud Run change saves, so the saving stays an estimate and monitoring ends "Inconclusive" rather than VERIFIED.

The Google Cloud integration page covers wiring up a project.

All OhChimp posts