GCP Cloud Run vs AWS Fargate: Serverless Containers Compared
A practical comparison of Cloud Run and Fargate for serverless container workloads, covering pricing, features, and when to choose each.
Serverless containers promise the best of both worlds: container flexibility without server management. But Cloud Run and Fargate have fundamentally different approaches. Here’s how they compare for real workloads.
Overview
| Aspect | Cloud Run | Fargate |
|---|---|---|
| Model | Request-driven, scale-to-zero | Task-based, minimum 1 task |
| Pricing | Per request + CPU-seconds | Per vCPU-hour + memory-hour |
| Startup | Cold starts (500ms-2s typical) | No cold starts (always running option) |
| Max timeout | 60 minutes (gen2) | Unlimited |
| Networking | Automatic HTTPS | Manual ALB/NLB setup |
| Complexity | Very low | Moderate |
Cloud Run: Request-Driven
Cloud Run scales based on incoming requests and scales to zero when idle.
Basic Deployment
# Deploy directly from source
gcloud run deploy my-service \
--source . \
--region us-central1 \
--allow-unauthenticated
# Or from container image
gcloud run deploy my-service \
--image gcr.io/my-project/my-app:v1 \
--region us-central1 \
--memory 512Mi \
--cpu 1 \
--min-instances 0 \
--max-instances 100 \
--concurrency 80 \
--timeout 300
Terraform
resource "google_cloud_run_service" "api" {
name = "api"
location = "us-central1"
template {
spec {
containers {
image = "gcr.io/my-project/api:v1"
resources {
limits = {
cpu = "1000m"
memory = "512Mi"
}
}
env {
name = "DATABASE_URL"
value_from {
secret_key_ref {
name = google_secret_manager_secret.db_url.secret_id
key = "latest"
}
}
}
ports {
container_port = 8080
}
}
container_concurrency = 80
timeout_seconds = 300
}
metadata {
annotations = {
"autoscaling.knative.dev/minScale" = "0"
"autoscaling.knative.dev/maxScale" = "100"
"run.googleapis.com/cpu-throttling" = "false"
}
}
}
traffic {
percent = 100
latest_revision = true
}
}
# IAM: Allow public access
resource "google_cloud_run_service_iam_member" "public" {
service = google_cloud_run_service.api.name
location = google_cloud_run_service.api.location
role = "roles/run.invoker"
member = "allUsers"
}
output "url" {
value = google_cloud_run_service.api.status[0].url
}
Cloud Run Features
# service.yaml
apiVersion: serving.knative.dev/v1
kind: Service
metadata:
name: my-service
spec:
template:
metadata:
annotations:
# Scale to zero
autoscaling.knative.dev/minScale: "0"
autoscaling.knative.dev/maxScale: "100"
# CPU allocation
run.googleapis.com/cpu-throttling: "false" # Always-on CPU
# Startup CPU boost
run.googleapis.com/startup-cpu-boost: "true"
# VPC connector
run.googleapis.com/vpc-access-connector: "projects/my-project/locations/us-central1/connectors/my-connector"
run.googleapis.com/vpc-access-egress: "private-ranges-only"
spec:
containerConcurrency: 80
timeoutSeconds: 300
containers:
- image: gcr.io/my-project/api:v1
resources:
limits:
cpu: "2"
memory: "2Gi"
AWS Fargate: Task-Based
Fargate runs tasks (containers) on demand. Works with ECS or EKS.
ECS with Fargate
# Task Definition
resource "aws_ecs_task_definition" "api" {
family = "api"
network_mode = "awsvpc"
requires_compatibilities = ["FARGATE"]
cpu = "256"
memory = "512"
execution_role_arn = aws_iam_role.ecs_execution.arn
task_role_arn = aws_iam_role.ecs_task.arn
container_definitions = jsonencode([
{
name = "api"
image = "${aws_ecr_repository.api.repository_url}:latest"
portMappings = [
{
containerPort = 8080
protocol = "tcp"
}
]
environment = [
{
name = "PORT"
value = "8080"
}
]
secrets = [
{
name = "DATABASE_URL"
valueFrom = aws_secretsmanager_secret.db_url.arn
}
]
logConfiguration = {
logDriver = "awslogs"
options = {
"awslogs-group" = aws_cloudwatch_log_group.api.name
"awslogs-region" = "us-east-1"
"awslogs-stream-prefix" = "api"
}
}
healthCheck = {
command = ["CMD-SHELL", "curl -f http://localhost:8080/health || exit 1"]
interval = 30
timeout = 5
retries = 3
startPeriod = 60
}
}
])
}
# ECS Service
resource "aws_ecs_service" "api" {
name = "api"
cluster = aws_ecs_cluster.main.id
task_definition = aws_ecs_task_definition.api.arn
desired_count = 2
launch_type = "FARGATE"
network_configuration {
subnets = aws_subnet.private[*].id
security_groups = [aws_security_group.api.id]
assign_public_ip = false
}
load_balancer {
target_group_arn = aws_lb_target_group.api.arn
container_name = "api"
container_port = 8080
}
deployment_circuit_breaker {
enable = true
rollback = true
}
}
# Auto Scaling
resource "aws_appautoscaling_target" "api" {
max_capacity = 10
min_capacity = 2
resource_id = "service/${aws_ecs_cluster.main.name}/${aws_ecs_service.api.name}"
scalable_dimension = "ecs:service:DesiredCount"
service_namespace = "ecs"
}
resource "aws_appautoscaling_policy" "api_cpu" {
name = "api-cpu-scaling"
policy_type = "TargetTrackingScaling"
resource_id = aws_appautoscaling_target.api.resource_id
scalable_dimension = aws_appautoscaling_target.api.scalable_dimension
service_namespace = aws_appautoscaling_target.api.service_namespace
target_tracking_scaling_policy_configuration {
predefined_metric_specification {
predefined_metric_type = "ECSServiceAverageCPUUtilization"
}
target_value = 70
scale_in_cooldown = 300
scale_out_cooldown = 60
}
}
Application Load Balancer
resource "aws_lb" "main" {
name = "api-alb"
internal = false
load_balancer_type = "application"
security_groups = [aws_security_group.alb.id]
subnets = aws_subnet.public[*].id
}
resource "aws_lb_target_group" "api" {
name = "api-tg"
port = 8080
protocol = "HTTP"
vpc_id = aws_vpc.main.id
target_type = "ip"
health_check {
enabled = true
healthy_threshold = 2
unhealthy_threshold = 3
timeout = 5
interval = 30
path = "/health"
matcher = "200"
}
}
resource "aws_lb_listener" "https" {
load_balancer_arn = aws_lb.main.arn
port = "443"
protocol = "HTTPS"
ssl_policy = "ELBSecurityPolicy-TLS-1-2-2017-01"
certificate_arn = aws_acm_certificate.api.arn
default_action {
type = "forward"
target_group_arn = aws_lb_target_group.api.arn
}
}
Pricing Comparison
Cloud Run
# Pay per request + CPU time
Request fee: $0.40 per million requests
CPU: $0.00002400 per vCPU-second
Memory: $0.00000250 per GiB-second
# Example: 1M requests/month, 200ms avg, 1 vCPU, 512MB
Requests: 1,000,000 × $0.0000004 = $0.40
CPU: 1,000,000 × 0.2s × $0.000024 = $4.80
Memory: 1,000,000 × 0.2s × 0.5 × $0.0000025 = $0.25
Total: ~$5.45/month
# Idle cost: $0 (scales to zero)
Fargate
# Pay per vCPU-hour + memory-hour
vCPU: $0.04048 per vCPU per hour
Memory: $0.004445 per GB per hour
# Example: 2 tasks running 24/7, 0.25 vCPU, 0.5GB each
vCPU: 2 × 0.25 × 720 hours × $0.04048 = $14.57
Memory: 2 × 0.5 × 720 hours × $0.004445 = $3.20
ALB: ~$16.20 (fixed) + $0.008 per LCU-hour
Total: ~$35-50/month minimum
# Idle cost: Same as running cost (no scale to zero by default)
Pricing Summary
| Scenario | Cloud Run | Fargate |
|---|---|---|
| Low traffic (< 1M req/mo) | ~$5-10 | ~$35-50 |
| Medium traffic (10M req/mo) | ~$50-100 | ~$50-100 |
| High traffic (100M req/mo) | ~$500-1000 | ~$200-500 |
| Idle/dev environments | ~$0 | ~$35-50 |
Key insight: Cloud Run wins for variable/low traffic. Fargate wins for consistent high traffic.
Feature Comparison
Networking
# Cloud Run: Automatic HTTPS endpoint
# my-service-xxxxx-uc.a.run.app
# Fargate: Requires ALB/NLB setup
# Needs VPC, subnets, security groups, target groups...
VPC Access
# Cloud Run: VPC Connector
resource "google_vpc_access_connector" "connector" {
name = "my-connector"
region = "us-central1"
ip_cidr_range = "10.8.0.0/28"
network = google_compute_network.main.name
}
# Fargate: Native VPC (awsvpc network mode)
# Containers get ENIs directly in your VPC
Secrets
# Cloud Run: Secret Manager integration
env {
name = "DB_PASSWORD"
value_from {
secret_key_ref {
name = "db-password"
key = "latest"
}
}
}
# Fargate: Secrets Manager or Parameter Store
secrets = [
{
name = "DB_PASSWORD"
valueFrom = "arn:aws:secretsmanager:us-east-1:xxx:secret:db-password"
}
]
Cold Starts
# Cloud Run: Min instances to reduce cold starts
annotations:
autoscaling.knative.dev/minScale: "1"
# Fargate: No cold starts (tasks always running)
# But: Scaling new tasks takes 30-60 seconds
Maximum Resources
| Resource | Cloud Run | Fargate |
|---|---|---|
| vCPU | 8 | 16 |
| Memory | 32 GB | 120 GB |
| Timeout | 60 min | Unlimited |
| Storage | 2 GB (in-memory) | 200 GB EBS |
When to Choose Cloud Run
✅ Choose Cloud Run when:
- Variable or unpredictable traffic
- Cost-sensitive workloads
- Simple HTTP services
- Fast deployments without infrastructure
- Development/staging environments
- Webhooks, APIs, microservices
❌ Avoid Cloud Run when:
- Long-running background jobs (>60 min)
- Consistent high throughput
- Need for persistent connections (WebSockets)
- Complex networking requirements
- GPU workloads
When to Choose Fargate
✅ Choose Fargate when:
- Consistent, predictable traffic
- Long-running processes
- Complex networking (VPC peering, PrivateLink)
- Already invested in AWS ecosystem
- Need persistent storage
- WebSocket connections
❌ Avoid Fargate when:
- Variable traffic with long idle periods
- Simple services (ALB overhead not worth it)
- Tight budget for dev/staging
- Team unfamiliar with ECS/networking
Migration Patterns
Cloud Run to Fargate
# Same container works on both
FROM node:20-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
COPY . .
EXPOSE 8080
CMD ["node", "server.js"]
// Handle differences in environment
const port = process.env.PORT || 8080;
// Cloud Run sets PORT
// Fargate uses container port mapping
server.listen(port, () => {
console.log(`Server running on port ${port}`);
});
Gradual Migration
# Run both during migration
# Route percentage of traffic to each
# Cloud Run (primary)
resource "google_cloud_run_service" "api" {
# ...
}
# DNS: Gradually shift traffic
# Week 1: 90% Cloud Run, 10% Fargate
# Week 2: 50% / 50%
# Week 3: 10% / 90%
# Week 4: 0% / 100%
Hybrid Architecture
Use both for their strengths:
┌─────────────────────────────────────────────┐
│ API Gateway │
└──────────────────────┬──────────────────────┘
│
┌─────────────┴─────────────┐
│ │
▼ ▼
┌───────────┐ ┌───────────┐
│ Cloud Run │ │ Fargate │
│ (APIs) │ │ (Workers) │
└───────────┘ └───────────┘
│ │
│ │
└──────────┬────────────────┘
│
┌─────▼─────┐
│ Database │
└───────────┘
# Cloud Run: HTTP APIs (scale to zero, pay per request)
# Fargate: Background workers (long-running, consistent load)
Key Takeaways
- Cloud Run is simpler — no VPC, ALB, or networking to configure
- Fargate is more flexible — better for complex architectures
- Cloud Run scales to zero — massive cost savings for variable traffic
- Fargate avoids cold starts — better for latency-sensitive apps
- Use both — Cloud Run for HTTP, Fargate for background jobs
- Same containers work on both — easy migration path
The “best” choice depends on your traffic patterns, team expertise, and existing cloud investments. For new projects with variable traffic, Cloud Run’s simplicity and scale-to-zero is compelling. For established AWS shops with consistent load, Fargate’s integration and flexibility wins.