Google Cloud Run: Serverless Containers That Scale to Zero
What Cloud Run Actually Does
Cloud Run runs your container and handles everything else: load balancing, auto-scaling (including scale-to-zero), TLS termination, and health checks. You push an image and get an HTTPS endpoint. Zero infrastructure to manage.
The constraint: your container must serve HTTP (or gRPC) on a port and handle requests within a configurable timeout (up to 60 minutes). It's not a replacement for persistent workloads or stateful services — it excels at stateless, request-driven services.
Two compute platforms:
- Cloud Run (fully managed): Google manages everything, scale-to-zero, pay per 100ms
- Cloud Run for Anthos: Runs on GKE, you manage the cluster, same API
Deploy Your First Service
# Authenticate
gcloud auth configure-docker
# Build and push to Artifact Registry
gcloud artifacts repositories create myapp \
--repository-format=docker \
--location=us-central1
docker build -t us-central1-docker.pkg.dev/my-project/myapp/api:v1 .
docker push us-central1-docker.pkg.dev/my-project/myapp/api:v1
# Deploy to Cloud Run
gcloud run deploy myapp-api \
--image us-central1-docker.pkg.dev/my-project/myapp/api:v1 \
--region us-central1 \
--platform managed \
--allow-unauthenticated \
--port 8080 \
--cpu 1 \
--memory 512Mi \
--min-instances 0 \
--max-instances 100 \
--concurrency 80 \
--timeout 300
Cloud Run prints the service URL after deploy. The first request may have a cold start (~100-500ms for most images).
Service Configuration (YAML)
For reproducible deployments, define services in YAML:
# service.yaml
apiVersion: serving.knative.dev/v1
kind: Service
metadata:
name: myapp-api
annotations:
run.googleapis.com/ingress: all
spec:
template:
metadata:
annotations:
autoscaling.knative.dev/minScale: "1"
autoscaling.knative.dev/maxScale: "100"
run.googleapis.com/cpu-throttling: "false" # Always-on CPU (no cold starts)
run.googleapis.com/startup-cpu-boost: "true" # Extra CPU during startup
spec:
containerConcurrency: 80
timeoutSeconds: 300
serviceAccountName: myapp-service-account@my-project.iam.gserviceaccount.com
containers:
- image: us-central1-docker.pkg.dev/my-project/myapp/api:v1
ports:
- containerPort: 8080
resources:
limits:
cpu: "2"
memory: 1Gi
env:
- name: APP_ENV
value: production
- name: DB_HOST
value: "10.0.0.5"
volumeMounts:
- name: secrets
mountPath: /secrets
volumes:
- name: secrets
secret:
secretName: db-password
gcloud run services replace service.yaml --region us-central1
Secrets from Secret Manager
Never put secrets in environment variables. Use Secret Manager:
# Create a secret
echo -n "my-db-password" | gcloud secrets create db-password \
--data-file=-
# Grant Cloud Run SA access
gcloud secrets add-iam-policy-binding db-password \
--member="serviceAccount:[email protected]" \
--role="roles/secretmanager.secretAccessor"
# Mount as file (recommended)
gcloud run deploy myapp-api \
--image us-central1-docker.pkg.dev/my-project/myapp/api:v1 \
--set-secrets /secrets/db-password=db-password:latest
# Or inject as environment variable
gcloud run deploy myapp-api \
--image us-central1-docker.pkg.dev/my-project/myapp/api:v1 \
--set-secrets DB_PASSWORD=db-password:latest
Using a specific version (instead of latest) is more secure — it prevents automatic secret rotation from affecting running instances without a redeploy:
--set-secrets DB_PASSWORD=db-password:3
Traffic Splitting and Canary Releases
Cloud Run supports splitting traffic between revisions — use this for canary deployments and A/B testing:
# Deploy a new revision without sending traffic to it
gcloud run deploy myapp-api \
--image us-central1-docker.pkg.dev/my-project/myapp/api:v2 \
--no-traffic \
--tag v2-candidate
# Test the new revision directly via its tagged URL
curl https://v2-candidate---myapp-api-<hash>.run.app/health
# Send 10% of traffic to the new revision
gcloud run services update-traffic myapp-api \
--to-revisions LATEST=10,myapp-api-00001-abc=90 \
--region us-central1
# Gradually increase
gcloud run services update-traffic myapp-api \
--to-revisions LATEST=50,myapp-api-00001-abc=50
# Full rollout
gcloud run services update-traffic myapp-api \
--to-latest \
--region us-central1
# Rollback: send 100% to previous revision
gcloud run services update-traffic myapp-api \
--to-revisions myapp-api-00001-abc=100
VPC Connectivity
By default, Cloud Run can't reach private resources (Cloud SQL private IP, Redis, Memorystore). Enable VPC access:
# Create a VPC connector
gcloud compute networks vpc-access connectors create myapp-connector \
--region us-central1 \
--network default \
--range 10.8.0.0/28
# Deploy with VPC connector
gcloud run deploy myapp-api \
--vpc-connector myapp-connector \
--vpc-egress private-ranges-only # Only route private IPs through VPC; public via internet
For Cloud SQL, use the Cloud SQL Auth Proxy pattern or Cloud SQL connectors:
# Using Cloud SQL connector (Python example)
gcloud run deploy myapp-api \
--set-env-vars INSTANCE_CONNECTION_NAME=my-project:us-central1:my-db \
--vpc-connector myapp-connector
Cloud Run Jobs (Batch Workloads)
Cloud Run Jobs run containers to completion — no HTTP server needed. Perfect for data pipelines, migrations, and scheduled tasks:
# Create a job
gcloud run jobs create my-migration-job \
--image us-central1-docker.pkg.dev/my-project/myapp/migrations:latest \
--region us-central1 \
--task-count 1 \
--max-retries 3 \
--set-env-vars DB_HOST=10.0.0.5 \
--set-secrets DB_PASSWORD=db-password:latest \
--vpc-connector myapp-connector
# Run it
gcloud run jobs execute my-migration-job --region us-central1
# Run with a Cloud Scheduler trigger (every day at 2am)
gcloud scheduler jobs create http daily-cleanup \
--schedule "0 2 * * *" \
--uri "https://us-central1-run.googleapis.com/apis/run.googleapis.com/v1/namespaces/my-project/jobs/my-cleanup-job:run" \
--message-body "{}" \
--oauth-service-account-email [email protected]
# Parallel job execution (10 tasks running concurrently)
gcloud run jobs create parallel-processor \
--task-count 100 \
--parallelism 10 # Run 10 at a time
Each task gets a CLOUD_RUN_TASK_INDEX env var (0-99 in this case) so tasks can process different data shards.
Authentication: Service-to-Service
Cloud Run services can require authentication — only requests with a valid Google-signed JWT are accepted:
# Deploy without --allow-unauthenticated
gcloud run deploy internal-api \
--image ... \
--region us-central1
# No --allow-unauthenticated flag
# Grant a service account permission to call it
gcloud run services add-iam-policy-binding internal-api \
--member="serviceAccount:[email protected]" \
--role="roles/run.invoker" \
--region us-central1
Calling an authenticated Cloud Run service:
# Get identity token for the calling service
TOKEN=$(gcloud auth print-identity-token \
--audiences=https://internal-api-<hash>.run.app)
curl -H "Authorization: Bearer $TOKEN" \
https://internal-api-<hash>.run.app/internal/process
In code (Python):
import google.auth.transport.requests
import google.oauth2.id_token
request = google.auth.transport.requests.Request()
target_audience = "https://internal-api-<hash>.run.app"
token = google.oauth2.id_token.fetch_id_token(request, target_audience)
headers = {"Authorization": f"Bearer {token}"}
Cost Optimization
Cloud Run bills for CPU and memory during request processing only (by default):
- $0.00002400/vCPU-second
- $0.00000250/GB-second
- First 180,000 vCPU-seconds and 360,000 GB-seconds free per month
Scale to zero: Set --min-instances 0. Cold starts add 100-500ms on first request after idle. Acceptable for most APIs.
CPU always allocated: --cpu-throttling=false keeps CPU allocated between requests (better for connection pooling, background tasks). Billed per 100ms even when idle — worth it for high-traffic services.
CPU Boost on startup: --startup-cpu-boost gives extra CPU during container startup to reduce cold start time. Free.
Minimum instances: Set --min-instances 1 to eliminate cold starts for SLA-sensitive services. One instance runs continuously (~$5-15/month depending on size) but eliminates latency variability.
Was this article helpful?
Senior Kubernetes Architect
10+ years orchestrating containers in production. Battle-tested opinions on everything from pod scheduling to service mesh. I've seen clusters burn and helped rebuild them better.
Related Articles
Google GKE: Production Kubernetes Cluster on Google Cloud
Launch a production GKE cluster on Google Cloud Platform — Autopilot vs Standard mode, node pools, Workload Identity, Binary Authorization, GKE Dataplane V2, private clusters, and Terraform configuration.
GCP Core Services: The DevOps Engineer's Essential Guide
Learn GCP's core services — Compute Engine, GKE, Cloud Storage, VPC, IAM, Cloud Build, and Cloud Run for modern DevOps workflows.
AWS ECS Fargate: Serverless Container Deployment Without Managing Nodes
Deploy containers on AWS ECS Fargate — no EC2 instances to manage. Covers task definitions, services, load balancers, environment variables, secrets from SSM/Secrets Manager, auto-scaling, and CI/CD integration.
Google Cloud Build: CI/CD Pipelines for GCP-Native Teams
Build, test, and deploy containerized apps with Google Cloud Build — triggers, substitutions, private pools, and GKE integration.
Alibaba Cloud ECS: Deploy and Manage Virtual Machines at Scale
Deploy and manage Alibaba Cloud ECS instances — instance families, storage types, security groups, auto-scaling groups, SLB load balancers, image management, and Terraform configuration for production workloads.
GCP Cloud Function "Function Execution Took Too Long" Error: Memory And Timeout Configuration Fix Guide
Getting the dreaded "Function execution took too long" error in your Cloud Functions? You're not alone. This error happens when your function exceeds the c...
More in GCP
View all →Fix GCP Cloud Storage '403 Forbidden' Errors
Troubleshoot and fix Google Cloud Storage 403 Forbidden errors caused by missing IAM roles, service account misconfiguration, or uniform bucket-level access.
GKE Autopilot: Serverless Kubernetes on Google Cloud
Deploy production workloads on GKE Autopilot — serverless Kubernetes with automatic node management, security, and scaling.