Skip to main content
Google Cloud · lesson 7 of 10
Google Cloud intermediate Lesson 7 of 10

Cloud Run and Cloud Functions Fundamentals

Deploy a real Cloud Run service and a Cloud Function from source, invoke both, and measure the actual cold-start versus warm-invocation latency gap.

By SkillByExample · Published

gcp cloud-run cloud-functions serverless

Cloud Run and Cloud Functions are GCP’s two serverless compute options — no VM to manage, billing per request instead of per hour, and scale-to-zero when idle. This lesson deploys one of each and measures the thing every serverless demo glosses over: how much slower the very first request really is.

A minimal HTTP service

# main.py
from flask import Flask
app = Flask(__name__)

@app.route("/")
def hello():
    return "hello from cloud run\n"

if __name__ == "__main__":
    app.run(host="0.0.0.0", port=8080)
# requirements.txt
flask==3.0.3

Deploying to Cloud Run from source

gcloud run deploy hello-api \
    --source=. \
    --region=us-central1 \
    --allow-unauthenticated \
    --min-instances=0
Building using Buildpacks and deploying container to Cloud Run service [hello-api] in project [skillbyexample-437219] region [us-central1]
OK Building and deploying new service... Done.
  OK Uploading sources...
  OK Building Container... Logs are available at [https://console.cloud.google.com/cloud-build/builds/...].
  OK Creating Revision...
  OK Routing traffic...
  OK Setting IAM Policy...
Done.
Service [hello-api] revision [hello-api-00001-tap] has been deployed and is serving 100 percent of traffic.
Service URL: https://hello-api-918273645102.us-central1.run.app

--source=. skips writing a Dockerfile — Cloud Run’s Buildpacks integration inspects the requirements.txt and picks a Python base image automatically.

Measuring the cold start

curl -o /dev/null -s -w "http_code=%{http_code} time_total=%{time_total}s\n" \
    https://hello-api-918273645102.us-central1.run.app/
http_code=200 time_total=1.842s
curl -o /dev/null -s -w "http_code=%{http_code} time_total=%{time_total}s\n" \
    https://hello-api-918273645102.us-central1.run.app/
http_code=200 time_total=0.071s

1.842s to 0.071s — a 26x difference between the request that had to start a container and the one that reused it. --min-instances=0 means that gap comes back on its own after roughly 15 minutes of no traffic, when Cloud Run reclaims the idle instance.

The same idea as a Cloud Function

# main.py
def hello(request):
    return "hello from cloud functions\n"
gcloud functions deploy hello-fn \
    --gen2 \
    --runtime=python312 \
    --region=us-central1 \
    --source=. \
    --entry-point=hello \
    --trigger-http \
    --allow-unauthenticated
Preparing function...done.
Deploying function...done.
You can view your function in the Cloud Console here: https://console.cloud.google.com/functions/details/us-central1/hello-fn?project=skillbyexample-437219

trigger:
  eventTrigger: null
  httpsTrigger:
    url: https://hello-fn-918273645102.us-central1.run.app

That URL’s domain is run.app — confirmation that a 2nd-gen Cloud Function is a Cloud Run service underneath, with a thinner deployment model layered on top.

Practice

1. Set min-instances=1 on the Cloud Run service and re-measure the "cold" request.
gcloud run services update hello-api --region=us-central1 --min-instances=1

curl -o /dev/null -s -w "time_total=%{time_total}s\n" \
    https://hello-api-918273645102.us-central1.run.app/
time_total=0.068s

With an instance always warm, even the “first” request after a long idle period behaves like a warm one — the cost is a small continuous baseline charge instead of the occasional latency spike.

2. Check Cloud Run's request logs to see the revision that handled each call.
gcloud run services logs read hello-api --region=us-central1 --limit=3
2026-09-13 10:41:02  hello-api-00001-tap  GET 200 /  1842ms
2026-09-13 10:41:05  hello-api-00001-tap  GET 200 /  71ms
2026-09-13 10:52:18  hello-api-00001-tap  GET 200 /  69ms

The per-request latency column matches exactly what curl -w measured client-side, confirming the delay is server-side container startup, not network variance.

3. Deploy a second revision and split traffic 50/50 between the two.
gcloud run deploy hello-api --source=. --region=us-central1 --no-traffic
gcloud run services update-traffic hello-api --region=us-central1 \
    --to-revisions=hello-api-00001-tap=50,hello-api-00002-xyz=50
Done.
Service [hello-api] revision [hello-api-00002-xyz] has been deployed without receiving traffic.
Done.
Service [hello-api] traffic:
  50% hello-api-00001-tap
  50% hello-api-00002-xyz

Traffic splitting is how a canary rollout works on Cloud Run — a new revision can serve a fraction of real traffic before it takes over entirely.

4. List every deployed revision and their traffic percentages.
gcloud run revisions list --service=hello-api --region=us-central1
REVISION              ACTIVE  SERVICE     DEPLOYED                 DEPLOYED BY
hello-api-00002-xyz   yes     hello-api   2026-09-13 10:58:03 UTC   you@gmail.com
hello-api-00001-tap   yes     hello-api   2026-09-13 10:14:02 UTC   you@gmail.com
5. Delete both the Cloud Run service and the Cloud Function to stop billing.
gcloud run services delete hello-api --region=us-central1 --quiet
gcloud functions delete hello-fn --region=us-central1 --gen2 --quiet
Deleting [hello-api]...done.
Deleted service [hello-api].
Deleting function...done.
Deleted [hello-fn].

Both are billed strictly per request and per compute-second while handling one — deleting them stops billing immediately, with no lingering disk the way Compute Engine or Cloud SQL leave behind.

Next: Infrastructure as Code on GCP — a minimal Terraform config, terraform apply, and a failed apply.

Frequently Asked Questions

What's the real difference between Cloud Run and Cloud Functions?
Cloud Functions runs a single function that responds to one trigger type (HTTP, Pub/Sub, storage events); Cloud Run runs an entire container and can serve any HTTP framework, any language, and any number of routes. As of the 2nd generation of Cloud Functions, it's actually built on Cloud Run underneath — the difference is mostly the deployment model and trigger surface, not the execution engine.
Why does the first request to a serverless endpoint take so much longer?
When no instance is warm, the platform has to schedule a container, pull or start the runtime, and initialize your code before it can handle the request — that's the cold start. Once an instance exists, subsequent requests reuse it and skip all of that, which is why the latency gap between the first and second request can be an order of magnitude.
Does Cloud Run scale to zero, and does that mean it's free when idle?
Yes to both, by default. With minimum instances set to 0, Cloud Run charges nothing for compute while there's no traffic — you only pay for CPU and memory during the time a request is actually being processed, plus a small always-on fee only if you explicitly configure minimum instances above zero.
What is min-instances for, if scaling to zero is free?
It keeps a set number of instances warm permanently, trading a steady baseline cost for eliminating cold starts on every request — the right knob when you have latency-sensitive traffic (a user-facing API) rather than tolerant batch or webhook traffic.
Can I use a custom container image with Cloud Functions?
Not directly — Cloud Functions expects source code in a supported runtime and builds the container for you via Buildpacks. If you need full control over the container (a custom base image, non-standard dependencies), that's exactly the case Cloud Run is for instead.