Cloud Run & Serverless Containers ยท Serverless Compute

Cloud Run

Intermediate โฑ 6โ€“8 hours 22 lessons 12 labs 24 interview Qs Production: Critical Cert: High

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
Alternatives
GKECompute EngineCloud Run JobsCloud FunctionsBatch
Related services
Artifact RegistryCloud SQLAlloyDBPub/SubCloud TasksSecret ManagerCloud Load Balancing
AWS equivalent

AWS Fargate (ECS) / App Runner

Azure equivalent

Azure Container Apps

Interactive architecture

Cloud Run โ€” architecture
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;
Drag to pan ยท scroll to zoom
A production Cloud Run topology: public traffic enters via Cloud Armor + external LB; the service reaches AlloyDB privately through Direct VPC egress; a second Cloud Run service consumes Pub/Sub with a dead-letter topic.
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. 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. 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. 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. 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. 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. 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

  1. Cloud Console โ†’ Cloud Run โ†’ Deploy container
  2. Choose an image from Artifact Registry (or 'Continuously deploy from a repository')
  3. Set region, CPU/memory, concurrency, min/max instances
  4. Under 'Authentication', choose 'Require authentication' for a private service
  5. Under 'Connections', enable Direct VPC egress and select your VPC/subnet
  6. Deploy, then copy the service URL

Code

bash
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
python
# ---------- 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()
bash
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?"}'
hcl
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"
}
โš ๏ธ min-instances > 0 bills even at zero traffic (a warm instance is always running). CPU-always-allocated bills for the full instance lifetime, not just request time. Set min-instances=0 for demos.
๐Ÿ”’ Use --no-allow-unauthenticated for anything non-public and grant run.invoker to specific principals. Give the service a dedicated least-privilege runtime service account โ€” never the default compute SA.

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

DimensionCloud RunGKE
Ops burdenNone โ€” fully managedYou manage clusters/nodes (less with Autopilot)
ScalingPer-request, scale-to-zeroPod/cluster autoscaling; scale-to-zero needs extra setup
Networking controlLimited (managed)Full: NetworkPolicy, service mesh, Gateway API
StatefulnessStateless onlyStatefulSets, persistent volumes
Best forAPIs, events, jobs, spiky trafficComplex 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

DimensionServiceJob
TriggerHTTP/gRPC/event requestManual, scheduled, or from Workflows
LifecycleServes traffic, scales with loadRuns to completion, then exits
TimeoutUp to ~60 min per requestLong-running (hours); parallel tasks
Use forAPIs, webhooks, push consumersBatch, ingestion, embeddings, migrations

Verdict. Serve requests with a Service; do finite heavy work with a Job.

Knowledge check

architectureQ1

You must expose a Cloud Run API to the internet but keep the ability to attach Cloud Armor WAF rules. What do you configure?

troubleshootingQ2

After deploy, Cloud Run returns 503 and the revision won't become ready. Most likely cause?

costQ3

A low-traffic internal API has latency spikes from cold starts. The cheapest fix that removes cold starts is:

service selectionQ4

You need to generate embeddings for 2 million documents in a one-off backfill that takes hours. Best fit?

Answer the questions to check your understanding.