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

AspectCloud RunFargate
ModelRequest-driven, scale-to-zeroTask-based, minimum 1 task
PricingPer request + CPU-secondsPer vCPU-hour + memory-hour
StartupCold starts (500ms-2s typical)No cold starts (always running option)
Max timeout60 minutes (gen2)Unlimited
NetworkingAutomatic HTTPSManual ALB/NLB setup
ComplexityVery lowModerate

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

ScenarioCloud RunFargate
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

ResourceCloud RunFargate
vCPU816
Memory32 GB120 GB
Timeout60 minUnlimited
Storage2 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

  1. Cloud Run is simpler — no VPC, ALB, or networking to configure
  2. Fargate is more flexible — better for complex architectures
  3. Cloud Run scales to zero — massive cost savings for variable traffic
  4. Fargate avoids cold starts — better for latency-sensitive apps
  5. Use both — Cloud Run for HTTP, Fargate for background jobs
  6. 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.