Cloud Run
Concept
Cloud Run is a fully managed serverless platform that runs stateless containers. You hand it a container image; it handles provisioning, TLS, autoscaling (including scale-to-zero), and request routing. It comes in two shapes: Services (request-driven, respond to HTTP/gRPC/events) and Jobs (run-to-completion tasks that don't serve traffic).
Problem it solves. Cloud Run removes almost all infrastructure operations for containerized workloads. You don't manage clusters, nodes, OS patching, or load balancers. You get per-request billing with scale-to-zero, so idle services cost nothing, while bursts scale out automatically to a configurable ceiling.
โ When to use
- Stateless, containerized request/response or event workloads
- Spiky or unpredictable traffic where scale-to-zero saves money
- You want minimal ops and fast deploys with revision-based rollout
- Batch/one-shot tasks that fit Cloud Run Jobs
โ When not to
- You need node-level control, custom schedulers, DaemonSets, or a service mesh โ GKE
- Long-lived stateful processes or persistent local disk beyond a request โ GKE/Compute Engine
- Sustained, steady high load where a reserved VM/GKE is cheaper
- Sub-millisecond, always-warm latency with zero cold-start tolerance and no min-instances budget
Use cases
- HTTP APIs and web backends (FastAPI, Flask, Express)
- Event consumers (Pub/Sub push, Eventarc, Cloud Tasks targets)
- Scheduled and batch work via Cloud Run Jobs
- Internal microservices reachable only inside a VPC
- AI inference gateways, RAG APIs, and agent services
Production use cases
- Private, authenticated RAG API fronted by an external load balancer + Cloud Armor
- Async document-processing worker triggered by Pub/Sub with a dead-letter topic
- Multi-tenant SaaS backend connecting privately to AlloyDB via Direct VPC egress
Limitations
- Request timeout ceiling (currently up to 60 minutes for services) โ heavy work belongs in Jobs/Batch
- Instances are stateless; local disk is an in-memory/ephemeral filesystem
- Cold starts add latency unless you set minimum instances
- Per-instance concurrency and CPU/memory ceilings constrain very large single requests
AWS Fargate (ECS) / App Runner
Azure Container Apps
Interactive architecture
Accessible text description
flowchart TD U[User] --> DNS[Cloud DNS] DNS --> ARM[Cloud Armor] ARM --> XLB[External Application LB] XLB --> CR[Cloud Run Service - FastAPI] CR -->|Direct VPC egress| PSC[Private Service Connect] PSC --> DB[(AlloyDB - private IP)] CR --> SM[Secret Manager] CR --> LOG[Cloud Logging] CR --> TR[Cloud Trace] PUB[Pub/Sub topic] -->|OIDC push| CR2[Cloud Run - worker] CR2 -.nack.-> DLQ((Dead-letter topic)) classDef gcp fill:#e8f0fe,stroke:#4285f4,color:#174ea6; classDef sec fill:#fce8e6,stroke:#ea4335,color:#a50e0e; class CR,CR2,DB,SM,LOG,TR,PSC,XLB,DNS gcp; class ARM,DLQ,PUB sec;
How it works
A request hits Cloud Run's front end, which routes it to a revision. If no warm instance exists, Cloud Run cold-starts a container. Each instance handles up to `concurrency` requests at once. IAM decides whether the caller may invoke the service before your code ever runs.
- 1 ยท Request arrivesCloud Run front end / load balancer
TLS is terminated and the request is routed to the target revision (or split across revisions for traffic splitting).
โ 503 if no capacity and max-instances is hit; 429 on overload ๐ request_count, request_latencies, container_instance_count ๐ฐ Requests are billed; ingress is free - 2 ยท Invocation authorization (IAM)IAM
For private services the caller must present an ID token and hold roles/run.invoker. Public services skip this. This happens before your container runs.
โ 403 if the caller lacks run.invoker ๐ Cloud Audit Logs - 3 ยท Instance selection / cold startCloud Run autoscaler
A warm instance handles the request if one is free (up to `concurrency`). Otherwise a new instance cold-starts: pull image, start container, pass health check.
โ Cold-start latency spikes; startup probe failures crash-loop the revision ๐ startup_latencies, instance count; set min-instances to avoid cold starts ๐ฐ CPU is billed during startup; min-instances bill even when idle - 4 ยท Application handles the requestYour container (FastAPI + Uvicorn)
Reads secrets from env/Secret Manager, connects to the database over a pooled connection, does work, emits structured JSON logs and trace spans with a correlation ID.
โ Unhandled exceptions โ 500; exhausted DB pool โ timeouts ๐ Structured logs, Cloud Trace spans, custom metrics ๐ฐ vCPU-seconds and memory-seconds billed while the request is in flight (and always-on if CPU-always-allocated) - 5 ยท Egress to backendsDirect VPC egress / connector
Outbound calls to AlloyDB/Cloud SQL/Memorystore route through the VPC to private IPs; external calls can go through Cloud NAT.
โ Connection refused if firewall/PSC/authorized-networks misconfigured ๐ VPC Flow Logs, DB connection metrics ๐ฐ Cloud NAT and cross-region egress are billed - 6 ยท Response & scale-downCloud Run autoscaler
The response is returned; idle instances are scaled down (to zero if min-instances=0) after a cool-down.
๐ SLO dashboards, error budgets ๐ฐ Scale-to-zero means no charge while idle
Hands-on example
Cloud Console
- Cloud Console โ Cloud Run โ Deploy container
- Choose an image from Artifact Registry (or 'Continuously deploy from a repository')
- Set region, CPU/memory, concurrency, min/max instances
- Under 'Authentication', choose 'Require authentication' for a private service
- Under 'Connections', enable Direct VPC egress and select your VPC/subnet
- Deploy, then copy the service URL
Code
PROJECT=my-project
REGION=us-central1
REPO=apps
IMAGE="$REGION-docker.pkg.dev/$PROJECT/$REPO/rag-api:latest"
gcloud config set project "$PROJECT"
gcloud services enable run.googleapis.com artifactregistry.googleapis.com
# Build & push with Cloud Build (no local Docker needed)
gcloud builds submit --tag "$IMAGE" .
# Deploy: private (no unauthenticated access), private DB egress, secrets, sane scaling
gcloud run deploy rag-api \
--image "$IMAGE" \
--region "$REGION" \
--no-allow-unauthenticated \
--service-account rag-api-sa@$PROJECT.iam.gserviceaccount.com \
--concurrency 40 \
--cpu 1 --memory 512Mi \
--min-instances 1 --max-instances 20 \
--timeout 120 \
--network projects/$PROJECT/global/networks/prod-vpc \
--subnet projects/$PROJECT/regions/$REGION/subnetworks/run-subnet \
--vpc-egress private-ranges-only \
--set-secrets DB_PASSWORD=alloydb-password:latest
# Grant a specific caller permission to invoke it
gcloud run services add-iam-policy-binding rag-api \
--region "$REGION" \
--member "serviceAccount:gateway-sa@$PROJECT.iam.gserviceaccount.com" \
--role roles/run.invoker # ---------- app/main.py : the FastAPI service ----------
import logging, json, os
from fastapi import FastAPI, Request
from pydantic import BaseModel
app = FastAPI()
logging.basicConfig(level=logging.INFO)
class Query(BaseModel):
question: str
@app.get("/healthz")
def health() -> dict:
return {"status": "ok"}
@app.post("/ask")
def ask(q: Query, request: Request) -> dict:
# Structured log with a correlation id (Cloud Run injects trace headers)
trace = request.headers.get("X-Cloud-Trace-Context", "")
logging.info(json.dumps({"event": "ask", "trace": trace, "q": q.question}))
return {"answer": f"You asked: {q.question}", "model": os.getenv("MODEL", "gemini")}
# ---------- client: invoking a PRIVATE service with an ID token ----------
import google.auth.transport.requests
import google.oauth2.id_token
import requests
def call_private(url: str, payload: dict) -> dict:
# Mint an OIDC ID token whose audience is the target URL (ADC identity)
auth_req = google.auth.transport.requests.Request()
token = google.oauth2.id_token.fetch_id_token(auth_req, url)
resp = requests.post(url + "/ask", json=payload,
headers={"Authorization": f"Bearer {token}"}, timeout=30)
resp.raise_for_status()
return resp.json() URL="https://rag-api-xxxx-uc.a.run.app"
TOKEN="$(gcloud auth print-identity-token --audiences=$URL)"
curl -s -X POST "$URL/ask" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"question":"How does Cloud Run scaling work?"}' resource "google_service_account" "rag_api" {
account_id = "rag-api-sa"
display_name = "RAG API runtime SA"
}
resource "google_cloud_run_v2_service" "rag_api" {
name = "rag-api"
location = var.region
ingress = "INGRESS_TRAFFIC_INTERNAL_LOAD_BALANCER" # private
template {
service_account = google_service_account.rag_api.email
scaling { min_instance_count = 1, max_instance_count = 20 }
vpc_access {
network_interfaces {
network = google_compute_network.vpc.id
subnetwork = google_compute_subnetwork.run_subnet.id
}
egress = "PRIVATE_RANGES_ONLY"
}
containers {
image = "${var.region}-docker.pkg.dev/${var.project_id}/apps/rag-api:latest"
resources { limits = { cpu = "1", memory = "512Mi" } }
env {
name = "DB_PASSWORD"
value_source { secret_key_ref { secret = "alloydb-password", version = "latest" } }
}
}
}
}
# Only the gateway SA may invoke it
resource "google_cloud_run_v2_service_iam_member" "invoker" {
name = google_cloud_run_v2_service.rag_api.name
location = var.region
role = "roles/run.invoker"
member = "serviceAccount:gateway-sa@${var.project_id}.iam.gserviceaccount.com"
} Verify
- curl the service without a token โ expect 403 (proves it's private)
- curl with a valid ID token โ expect 200 and your JSON response
- Cloud Run โ Logs: confirm structured JSON logs with the trace id
- Cloud Run โ Metrics: watch instance count scale with load
Cleanup
- gcloud run services delete rag-api --region us-central1
- Delete the Artifact Registry image and the runtime service account
- Remove the VPC subnet/connector if created only for the demo
Troubleshooting
- 503 on deploy โ container didn't listen on $PORT or failed the startup probe
- 403 invoking โ caller lacks roles/run.invoker, or ID-token audience โ service URL
- Cold-start latency โ raise min-instances; trim image size and startup work
- Can't reach AlloyDB โ check Direct VPC egress, firewall, and PSC/authorized networks
- Timeouts on long work โ move it to a Cloud Run Job or Batch; don't extend request timeouts indefinitely
Production considerations
Scalability
Scales per-request from 0 to max-instances. Tune concurrency (requests per instance) with max-instances to bound cost and protect backends (e.g. cap DB connections = max-instances ร pool size).
Availability
Regional and multi-zone by default within a region. For higher availability, deploy to multiple regions behind a global external load balancer.
Performance
Watch p95/p99 latency and startup_latencies. Reduce cold starts with min-instances, smaller images, and lazy imports. Right-size CPU/memory to your request profile.
Reliability
Use revisions + traffic splitting for safe rollout and instant rollback. Add retries with backoff for downstream calls and make handlers idempotent.
Networking
Prefer private ingress (internal / internal-and-LB) plus Direct VPC egress to reach private backends. Front public services with an external LB + Cloud Armor.
Security
Dedicated runtime SA with least privilege; secrets via Secret Manager; require authentication; keep images scanned in Artifact Registry with Binary Authorization if needed.
Monitoring
Structured JSON logs, correlation IDs, Cloud Trace, and SLOs on latency/availability with alerting on the error budget.
Cost
Scale-to-zero for spiky workloads; measure cost per request. Compare against a reserved VM/GKE for steady high load. Beware CPU-always-allocated and min-instances charges.
Quotas
Per-region max instances and concurrent requests have quotas โ request increases before launch load.
Data protection
Cloud Run is stateless; persist state in databases/Cloud Storage with their own backup and retention policies.
Disaster recovery
Redeploy is fast (image + config). For regional failure, run active-active in โฅ2 regions behind a global LB; define RTO/RPO on the data tier, not the compute tier.
Multi-region
Deploy identical services per region; use a global external LB for routing and failover. Keep data replication cost and latency in mind.
What interviewers expect you to know
Definitions
- Cloud Run = managed serverless containers; Services (request-driven) vs Jobs (run-to-completion)
- Concurrency = max simultaneous requests per instance; Revision = an immutable deployment
Design questions
- Design a private FastAPI + AlloyDB service with no public IPs
- How would you make a Cloud Run service multi-region and highly available?
- How do you consume Pub/Sub reliably on Cloud Run (push vs pull, DLQ, idempotency)?
Trade-offs
- Cloud Run vs GKE: ops simplicity & scale-to-zero vs cluster control & flexibility
- Concurrency high (cheaper, shared CPU) vs low (isolation, predictable latency)
- min-instances (no cold starts, always-billed) vs scale-to-zero (cheap, cold starts)
Failure scenarios
- Downstream DB pool exhausted under scale-out โ how do you bound it?
- Cold-start latency spikes during a traffic burst โ mitigations?
- A revision crash-loops on deploy โ how do you detect and roll back?
Common follow-ups
- How is service-to-service auth done? (ID tokens + run.invoker)
- How do you keep the service private but internet-reachable? (internal-and-LB + external LB)
- How do you handle work longer than the request timeout? (Jobs/Batch + async pattern)
Misconceptions
- 'Serverless means no cold starts' โ false without min-instances
- 'Cloud Run can't be private' โ false; use internal ingress + Direct VPC egress
- 'One instance = one request' โ only if concurrency=1; default handles many
๐ฌ A strong answer
Cloud Run runs stateless containers with per-request autoscaling and scale-to-zero. I'd deploy FastAPI as a private service (internal ingress) with a dedicated least-privilege SA, reach AlloyDB via Direct VPC egress to a private IP, keep secrets in Secret Manager, and bound cost by pairing concurrency with max-instances so DB connections stay within limits. For work beyond the request timeout I'd offload to a Cloud Run Job triggered via Pub/Sub with a dead-letter topic, tracking state in a jobs table the client polls. I'd roll out with revisions + traffic splitting and watch p99 latency and the error budget.
Common mistakes
- Deploying with --allow-unauthenticated by accident, exposing an internal API
- Using the default compute service account instead of a dedicated least-privilege SA
- Not bounding max-instances โ connection storms that exhaust the database
- Trying to run long jobs inside a Service instead of a Cloud Run Job / Batch
- Baking secrets into the image or env instead of Secret Manager references
- Ignoring cold starts for latency-sensitive endpoints (no min-instances)
- Forgetting idempotency on Pub/Sub push consumers (at-least-once delivery)
- No dead-letter topic, so poison messages retry forever
Comparisons
Cloud Run vs GKE
| Dimension | Cloud Run | GKE |
|---|---|---|
| Ops burden | None โ fully managed | You manage clusters/nodes (less with Autopilot) |
| Scaling | Per-request, scale-to-zero | Pod/cluster autoscaling; scale-to-zero needs extra setup |
| Networking control | Limited (managed) | Full: NetworkPolicy, service mesh, Gateway API |
| Statefulness | Stateless only | StatefulSets, persistent volumes |
| Best for | APIs, events, jobs, spiky traffic | Complex platforms, mesh, steady heavy load |
Verdict. Default to Cloud Run; reach for GKE when you need node-level control, statefulness, or a mesh.
Cloud Run Service vs Cloud Run Job
| Dimension | Service | Job |
|---|---|---|
| Trigger | HTTP/gRPC/event request | Manual, scheduled, or from Workflows |
| Lifecycle | Serves traffic, scales with load | Runs to completion, then exits |
| Timeout | Up to ~60 min per request | Long-running (hours); parallel tasks |
| Use for | APIs, webhooks, push consumers | Batch, ingestion, embeddings, migrations |
Verdict. Serve requests with a Service; do finite heavy work with a Job.
Knowledge check
You must expose a Cloud Run API to the internet but keep the ability to attach Cloud Armor WAF rules. What do you configure?
After deploy, Cloud Run returns 503 and the revision won't become ready. Most likely cause?
A low-traffic internal API has latency spikes from cold starts. The cheapest fix that removes cold starts is:
You need to generate embeddings for 2 million documents in a one-off backfill that takes hours. Best fit?