“Just use serverless” and “containers are cheaper at scale” are both oversimplifications. The truth depends on your traffic patterns, execution time, and operational costs. This guide provides a framework for making the right choice — with real numbers.

Cost Models Compared

Lambda Pricing

lambda_pricing:
  requests: $0.20 per 1M requests
  compute:
    memory_gb_second: $0.0000166667
    # CPU scales with memory

  example_128mb_1s:
    per_request: $0.0000020  # request cost
    per_compute: $0.0000021  # 0.125GB × 1s × $0.0000166667
    total: $0.0000041

  example_1024mb_200ms:
    per_request: $0.0000020
    per_compute: $0.0000033  # 1GB × 0.2s × $0.0000166667
    total: $0.0000053

  free_tier:
    requests: 1M per month
    compute: 400,000 GB-seconds per month

Fargate Pricing

fargate_pricing:
  vcpu_per_hour: $0.04048
  memory_gb_per_hour: $0.004445

  example_0.5vcpu_1gb:
    per_hour: $0.02469  # ($0.04048 × 0.5) + ($0.004445 × 1)
    per_month_24x7: $18.02

  example_1vcpu_2gb:
    per_hour: $0.04937
    per_month_24x7: $36.04

  spot_discount: "~70% off"
  spot_example_1vcpu_2gb:
    per_month_24x7: ~$10.81

EKS/EC2 Pricing

eks_pricing:
  cluster_fee: $0.10/hour ($73/month)
  worker_nodes: EC2 pricing (varies)

  example_m6i.large_on_demand:
    per_hour: $0.096
    per_month: $70.08

  example_m6i.large_reserved_1yr:
    per_month: ~$49  # 30% discount

  example_m6i.large_spot:
    per_month: ~$25  # 65% discount

Break-Even Calculator

Python Calculator

from dataclasses import dataclass
from typing import Optional

@dataclass
class LambdaConfig:
    memory_mb: int
    execution_ms: int
    requests_per_month: int

@dataclass
class FargateConfig:
    vcpu: float
    memory_gb: float
    tasks: int
    hours_per_month: float = 730  # 24x7

@dataclass
class EKSConfig:
    instance_type: str
    instance_cost_per_hour: float
    node_count: int
    hours_per_month: float = 730
    cluster_fee: float = 73.0  # $0.10/hour

def calculate_lambda_cost(config: LambdaConfig) -> float:
    """Calculate monthly Lambda cost."""
    # Request cost
    request_cost = (config.requests_per_month / 1_000_000) * 0.20

    # Compute cost
    memory_gb = config.memory_mb / 1024
    duration_seconds = config.execution_ms / 1000
    gb_seconds = memory_gb * duration_seconds * config.requests_per_month
    compute_cost = gb_seconds * 0.0000166667

    # Subtract free tier (first month only concept, ignore for comparison)

    return request_cost + compute_cost

def calculate_fargate_cost(config: FargateConfig, spot: bool = False) -> float:
    """Calculate monthly Fargate cost."""
    vcpu_cost = config.vcpu * 0.04048 * config.hours_per_month * config.tasks
    memory_cost = config.memory_gb * 0.004445 * config.hours_per_month * config.tasks
    total = vcpu_cost + memory_cost

    if spot:
        total *= 0.3  # 70% discount

    return total

def calculate_eks_cost(config: EKSConfig) -> float:
    """Calculate monthly EKS cost."""
    node_cost = config.instance_cost_per_hour * config.hours_per_month * config.node_count
    return config.cluster_fee + node_cost

def find_break_even(
    lambda_config: LambdaConfig,
    container_cost: float
) -> int:
    """Find requests/month where Lambda cost equals container cost."""
    # Cost per request
    memory_gb = lambda_config.memory_mb / 1024
    duration_seconds = lambda_config.execution_ms / 1000

    cost_per_request = (
        0.20 / 1_000_000 +  # Request
        memory_gb * duration_seconds * 0.0000166667  # Compute
    )

    # Break even = container_cost / cost_per_request
    return int(container_cost / cost_per_request)


# Example analysis
def analyze_workload():
    """Compare costs for a typical API workload."""
    print("=" * 60)
    print("Cost Comparison: API Workload")
    print("=" * 60)

    # Lambda: 512MB, 200ms average
    lambda_configs = [
        (100_000, "100K requests/month"),
        (1_000_000, "1M requests/month"),
        (10_000_000, "10M requests/month"),
        (100_000_000, "100M requests/month"),
    ]

    # Fargate: 0.5 vCPU, 1GB, 2 tasks (HA)
    fargate_cost = calculate_fargate_cost(
        FargateConfig(vcpu=0.5, memory_gb=1, tasks=2)
    )
    fargate_spot_cost = calculate_fargate_cost(
        FargateConfig(vcpu=0.5, memory_gb=1, tasks=2),
        spot=True
    )

    # EKS: 2x t3.medium nodes
    eks_cost = calculate_eks_cost(
        EKSConfig(
            instance_type="t3.medium",
            instance_cost_per_hour=0.0416,
            node_count=2
        )
    )

    print(f"\nContainer costs (monthly, 24x7):")
    print(f"  Fargate (On-Demand): ${fargate_cost:.2f}")
    print(f"  Fargate (Spot):      ${fargate_spot_cost:.2f}")
    print(f"  EKS (On-Demand):     ${eks_cost:.2f}")

    print(f"\nLambda costs (512MB, 200ms avg):")
    for requests, label in lambda_configs:
        lambda_cost = calculate_lambda_cost(
            LambdaConfig(memory_mb=512, execution_ms=200, requests_per_month=requests)
        )
        print(f"  {label}: ${lambda_cost:.2f}")

    # Find break-even
    break_even = find_break_even(
        LambdaConfig(memory_mb=512, execution_ms=200, requests_per_month=0),
        fargate_cost
    )
    print(f"\nBreak-even point (Lambda vs Fargate On-Demand):")
    print(f"  {break_even:,} requests/month")

    break_even_spot = find_break_even(
        LambdaConfig(memory_mb=512, execution_ms=200, requests_per_month=0),
        fargate_spot_cost
    )
    print(f"\nBreak-even point (Lambda vs Fargate Spot):")
    print(f"  {break_even_spot:,} requests/month")


if __name__ == "__main__":
    analyze_workload()

Output Example

============================================================
Cost Comparison: API Workload
============================================================

Container costs (monthly, 24x7):
  Fargate (On-Demand): $36.04
  Fargate (Spot):      $10.81
  EKS (On-Demand):     $133.74

Lambda costs (512MB, 200ms avg):
  100K requests/month: $0.19
  1M requests/month: $1.89
  10M requests/month: $18.87
  100M requests/month: $188.67

Break-even point (Lambda vs Fargate On-Demand):
  19,105,263 requests/month

Break-even point (Lambda vs Fargate Spot):
  5,731,578 requests/month

Traffic Pattern Analysis

Steady Traffic

For consistent 24x7 traffic:
- < 5M requests/month: Lambda wins
- 5-20M requests/month: Depends on configuration
- > 20M requests/month: Containers likely win

But consider: Can containers handle your peak?

Bursty Traffic

For traffic with 10x peaks:
- Lambda handles bursts automatically
- Containers need provisioned capacity for peaks
- OR containers need complex autoscaling

Example:
- Average: 100 req/sec
- Peak: 1000 req/sec (2 hours/day)

Lambda cost: Based on actual requests
Container cost: Must provision for peak capacity

Visualization

Monthly Requests vs Cost

Cost ($)
|
300 |                                    ╱ Lambda
    |                                  ╱
200 |                                ╱
    |                              ╱
150 |         ╱─────────────────╱─────── Fargate
    |       ╱
100 |     ╱
    |   ╱
 50 | ╱
    |___________________________________________
      1M   5M   10M   20M   50M   100M  Requests

Break-even: ~19M requests/month (Lambda 512MB/200ms vs Fargate 0.5vCPU/1GB)

Hidden Costs

Serverless Hidden Costs

serverless_hidden_costs:
  cold_starts:
    impact: "100-500ms latency on cold start"
    mitigation: "Provisioned concurrency ($)"

  provisioned_concurrency:
    cost: "$0.000004463 per GB-second"
    example_10_warm: "~$30/month for 10 instances warm"

  api_gateway:
    http_api: "$1.00 per million requests"
    rest_api: "$3.50 per million requests"

  nat_gateway:
    per_gb: "$0.045"
    per_hour: "$0.045"
    warning: "VPC Lambda + NAT can be expensive"

  cloudwatch_logs:
    ingestion: "$0.50 per GB"
    storage: "$0.03 per GB/month"

Container Hidden Costs

container_hidden_costs:
  load_balancer:
    alb_per_hour: "$0.0225"
    alb_per_month: "$16.43 minimum"
    lcu_cost: "$0.008 per LCU-hour"

  nat_gateway:
    same_as_lambda: "If containers in private subnet"

  ecr:
    storage: "$0.10 per GB/month"
    transfer: "Standard data transfer rates"

  monitoring:
    container_insights: "$0.30 per container/month"

  cluster_management:
    eks_fee: "$0.10/hour = $73/month"
    ecs_fee: "Free"

  operational_overhead:
    patching: "OS and runtime updates"
    security_scanning: "Container image scanning"
    orchestration: "Service mesh, secrets, networking"

Decision Framework

Use Serverless When

serverless_good_fit:
  traffic_patterns:
    - Unpredictable or highly variable traffic
    - Significant idle periods
    - < 10M requests/month

  requirements:
    - Fast time-to-market
    - Minimal ops overhead
    - < 15 minute execution time
    - Event-driven architecture

  team:
    - Small team
    - Limited DevOps capacity
    - Focus on features over infrastructure

Use Containers When

containers_good_fit:
  traffic_patterns:
    - Consistent, predictable traffic
    - > 20M requests/month
    - Long-running processes

  requirements:
    - Specific runtime requirements
    - Need for GPUs
    - Stateful applications
    - Complex networking requirements

  team:
    - Dedicated platform team
    - Kubernetes expertise
    - Existing container infrastructure

Terraform Decision Module

# decision.tf - Compute deployment based on traffic
variable "monthly_requests" {
  type        = number
  description = "Expected monthly requests"
}

variable "avg_execution_ms" {
  type        = number
  description = "Average execution time in ms"
}

variable "memory_mb" {
  type        = number
  description = "Required memory in MB"
}

locals {
  # Calculate Lambda cost
  lambda_cost_per_request = (
    0.20 / 1000000 +  # Request
    (var.memory_mb / 1024) * (var.avg_execution_ms / 1000) * 0.0000166667  # Compute
  )

  lambda_monthly_cost = local.lambda_cost_per_request * var.monthly_requests

  # Fargate cost (0.5 vCPU, 1GB, 2 tasks)
  fargate_monthly_cost = 2 * 730 * ((0.5 * 0.04048) + (1 * 0.004445))

  # Decision
  use_lambda = local.lambda_monthly_cost < local.fargate_monthly_cost

  deployment_recommendation = local.use_lambda ? "lambda" : "fargate"
}

output "cost_analysis" {
  value = {
    lambda_monthly    = local.lambda_monthly_cost
    fargate_monthly   = local.fargate_monthly_cost
    recommendation    = local.deployment_recommendation
    savings           = abs(local.lambda_monthly_cost - local.fargate_monthly_cost)
  }
}

Hybrid Approach

Best of Both Worlds

hybrid_architecture:
  lambda:
    - API endpoints (< 29 seconds)
    - Event processing
    - Scheduled tasks
    - Infrequent operations

  containers:
    - Long-running services
    - Background workers
    - ML inference
    - Stateful workloads

  shared:
    - Same VPC
    - Same data stores
    - Common monitoring
    - Single deployment pipeline

Example Architecture

                        ┌────────────────────────────────────┐
                        │         API Gateway                 │
                        └───────────────┬────────────────────┘

                    ┌───────────────────┼───────────────────┐
                    ▼                   ▼                   ▼
            ┌──────────────┐   ┌──────────────┐   ┌──────────────┐
            │   Lambda     │   │   Lambda     │   │   Lambda     │
            │  Auth/CRUD   │   │   Webhooks   │   │  Processing  │
            └──────────────┘   └──────────────┘   └──────┬───────┘


                                                  ┌──────────────┐
                                                  │     SQS      │
                                                  └──────┬───────┘


                                               ┌──────────────────┐
                                               │    Fargate       │
                                               │  Heavy Workers   │
                                               │  (Spot)          │
                                               └──────────────────┘

Key Takeaways

  1. Calculate, don’t assume — break-even varies by workload
  2. Include hidden costs — NAT Gateway, ALB, API Gateway add up
  3. Traffic pattern matters — bursty favors Lambda, steady favors containers
  4. Consider operational cost — your time has value
  5. Hybrid is often optimal — use the right tool for each component

“The cheapest architecture is the one that matches your traffic pattern. Everything else is either over-provisioning or under-engineering.”