DEV Community

Cover image for AWS Data Transfer Pricing Makes No Sense - Until You See This Cheat Sheet πŸ—ΊοΈ
Suhas Mallesh
Suhas Mallesh

Posted on

AWS Data Transfer Pricing Makes No Sense - Until You See This Cheat Sheet πŸ—ΊοΈ

AWS charges differently for same-AZ, cross-AZ, cross-region, and internet traffic - and most teams have no idea which one is bleeding money. Here's every scenario explained with Terraform patterns to minimize each.

Ask any AWS engineer: "How much does data transfer cost?"

Watch them struggle πŸ˜•

That's because AWS has at least 15 different data transfer prices depending on where data goes, which service sends it, and whether you remembered to use a VPC endpoint.

Nobody memorizes this. So I built the cheat sheet I wish I had, plus Terraform patterns to fix each one.

πŸ“Š The Master Cheat Sheet

Bookmark this. You'll need it.

Traffic Within AWS

From β†’ To Cost/GB Notes
EC2 β†’ EC2 (same AZ) Free βœ… Use private IP only
EC2 β†’ EC2 (same AZ, public IP) $0.02 Private IP = free, public IP = paid 🀦
EC2 β†’ EC2 (cross-AZ) $0.02 $0.01 out + $0.01 in
EC2 β†’ EC2 (cross-region) $0.02 Varies by region pair
EC2 β†’ S3 (same region) Free βœ… Via VPC Gateway Endpoint
EC2 β†’ S3 (same region, no endpoint) $0.09 Goes through NAT Gateway πŸ’Έ
EC2 β†’ DynamoDB (same region) Free βœ… Via VPC Gateway Endpoint
EC2 β†’ RDS (same AZ) Free βœ…
EC2 β†’ RDS (cross-AZ) $0.02 Multi-AZ adds this automatically
S3 β†’ CloudFront Free βœ… CloudFront origin fetches are free
S3 β†’ Internet $0.09 First 10TB/month
CloudFront β†’ Internet $0.085 Cheaper than S3 direct + 1TB/month free
NAT Gateway processing $0.045 On top of any other transfer fees

Traffic to Internet

Volume/Month S3/EC2 Direct Via CloudFront
First 1TB $0.09/GB Free (free tier) βœ…
1-10TB $0.09/GB $0.085/GB
10-50TB $0.085/GB $0.080/GB
50-150TB $0.070/GB $0.060/GB

CloudFront is cheaper than direct S3 at every tier. Yes, adding a CDN saves money. 🀯

πŸ’Έ The 5 Biggest Data Transfer Traps

Trap #1: NAT Gateway Double Tax

This is the most expensive mistake teams make.

Private EC2 β†’ NAT Gateway β†’ Internet β†’ S3

You pay:
  NAT Gateway processing: $0.045/GB
  Data transfer out:      $0.09/GB
  Total:                  $0.135/GB  😱

Fix with VPC Endpoint:
  Private EC2 β†’ VPC Endpoint β†’ S3
  Total:                  $0.00/GB   βœ…
Enter fullscreen mode Exit fullscreen mode

Terraform fix:

# FREE S3 access from private subnets
resource "aws_vpc_endpoint" "s3" {
  vpc_id       = aws_vpc.main.id
  service_name = "com.amazonaws.${var.region}.s3"
  vpc_endpoint_type = "Gateway"

  route_table_ids = var.private_route_table_ids

  tags = { Name = "s3-gateway-endpoint" }
}

# FREE DynamoDB access from private subnets
resource "aws_vpc_endpoint" "dynamodb" {
  vpc_id       = aws_vpc.main.id
  service_name = "com.amazonaws.${var.region}.dynamodb"
  vpc_endpoint_type = "Gateway"

  route_table_ids = var.private_route_table_ids

  tags = { Name = "dynamodb-gateway-endpoint" }
}
Enter fullscreen mode Exit fullscreen mode

Savings: $0.135/GB β†’ $0.00/GB. For 1TB/month of S3 traffic = $135/month saved.

Trap #2: Cross-AZ Traffic You Don't Know About

Multi-AZ is great for availability. But every byte between AZs costs $0.02/GB.

Common hidden cross-AZ traffic:

  • App server in AZ-a talking to Redis in AZ-b
  • Microservices spread across AZs calling each other
  • RDS Multi-AZ synchronous replication

Terraform fix β€” Keep tightly coupled services in the same AZ:

# Pin your app and its cache to the same AZ
resource "aws_instance" "app" {
  subnet_id = aws_subnet.private_az_a.id  # AZ-a
  # ...
}

resource "aws_elasticache_replication_group" "redis" {
  preferred_cache_cluster_azs = ["us-east-1a"]  # Same AZ
  # ...
}

# For ECS β€” use placement constraints
resource "aws_ecs_service" "api" {
  # ...
  placement_constraints {
    type       = "memberOf"
    expression = "attribute:ecs.availability-zone == us-east-1a"
  }
}
Enter fullscreen mode Exit fullscreen mode

⚠️ Trade-off: Pinning to one AZ reduces availability. Do this for dev/staging, not production. In prod, accept cross-AZ cost as the price of HA.

Trap #3: Public IP When Private IP Works

This one is sneaky. Two EC2 instances in the same AZ communicating via public IP costs $0.02/GB. Via private IP? Free.

EC2-A (public IP) β†’ Internet Gateway β†’ EC2-B (public IP)
Cost: $0.02/GB

EC2-A (private IP) β†’ EC2-B (private IP)
Cost: $0.00/GB βœ…
Enter fullscreen mode Exit fullscreen mode

Terraform fix:

# Don't assign public IPs to instances that don't need them
resource "aws_instance" "backend" {
  subnet_id                   = aws_subnet.private.id
  associate_public_ip_address = false  # πŸ‘ˆ No public IP

  tags = { Name = "backend-server" }
}

# Use internal ALB for service-to-service communication
resource "aws_lb" "internal" {
  name               = "internal-api"
  internal           = true  # πŸ‘ˆ Internal, no internet-facing
  load_balancer_type = "application"
  subnets            = var.private_subnet_ids
}
Enter fullscreen mode Exit fullscreen mode

Trap #4: S3 Direct Download Instead of CloudFront

If you're serving files to users directly from S3, you're paying more than if you put CloudFront in front.

S3 β†’ Internet:         $0.09/GB + no free tier
CloudFront β†’ Internet: $0.085/GB + 1TB/month FREE
Enter fullscreen mode Exit fullscreen mode

CloudFront is literally cheaper AND faster. There's no reason not to use it for public assets.

Terraform fix:

resource "aws_cloudfront_distribution" "assets" {
  enabled         = true
  is_ipv6_enabled = true

  origin {
    domain_name              = aws_s3_bucket.assets.bucket_regional_domain_name
    origin_id                = "s3-assets"
    origin_access_control_id = aws_cloudfront_origin_access_control.s3.id
  }

  default_cache_behavior {
    allowed_methods        = ["GET", "HEAD"]
    cached_methods         = ["GET", "HEAD"]
    target_origin_id       = "s3-assets"
    viewer_protocol_policy = "redirect-to-https"

    forwarded_values {
      query_string = false
      cookies { forward = "none" }
    }

    # Cache for 24 hours
    min_ttl     = 0
    default_ttl = 86400
    max_ttl     = 604800
  }

  restrictions {
    geo_restriction { restriction_type = "none" }
  }

  viewer_certificate {
    cloudfront_default_certificate = true
  }

  tags = { Name = "asset-cdn" }
}

resource "aws_cloudfront_origin_access_control" "s3" {
  name                              = "s3-assets-oac"
  origin_access_control_origin_type = "s3"
  signing_behavior                  = "always"
  signing_protocol                  = "sigv4"
}
Enter fullscreen mode Exit fullscreen mode

Savings on 2TB/month: S3 direct = $180/mo β†’ CloudFront = $85/mo (first 1TB free). $95/month saved. πŸ’°

Trap #5: Cross-Region Replication Without Thinking

S3 cross-region replication, RDS cross-region read replicas, DynamoDB global tables β€” all incur $0.02/GB transfer.

S3 Replication: us-east-1 β†’ eu-west-1
100GB/month of changes = $2/month (seems cheap)
10TB/month of changes = $200/month (not cheap) 😬
Enter fullscreen mode Exit fullscreen mode

Terraform fix β€” Replicate selectively:

resource "aws_s3_bucket_replication_configuration" "selective" {
  bucket = aws_s3_bucket.primary.id
  role   = aws_iam_role.replication.arn

  rule {
    id     = "critical-data-only"
    status = "Enabled"

    # Only replicate what matters, not everything
    filter {
      prefix = "critical/"  # πŸ‘ˆ Not the entire bucket
    }

    destination {
      bucket        = aws_s3_bucket.replica.arn
      storage_class = "STANDARD_IA"  # Cheaper storage in replica
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

πŸ“Š Data Transfer Cost Monitor

Deploy this to track where your data transfer money goes:

# monitoring/data-transfer.tf

resource "aws_ce_anomaly_monitor" "data_transfer" {
  name              = "data-transfer-anomaly-monitor"
  monitor_type      = "DIMENSIONAL"
  monitor_dimension = "SERVICE"
}

resource "aws_ce_anomaly_subscription" "data_transfer" {
  name = "data-transfer-alerts"

  monitor_arn_list = [aws_ce_anomaly_monitor.data_transfer.arn]

  frequency = "DAILY"

  threshold_expression {
    dimension {
      key           = "ANOMALY_TOTAL_IMPACT_ABSOLUTE"
      values        = ["50"]  # Alert if anomaly > $50
      match_options = ["GREATER_THAN_OR_EQUAL"]
    }
  }

  subscriber {
    type    = "EMAIL"
    address = var.alert_email
  }
}

# CloudWatch dashboard for data transfer visibility
resource "aws_cloudwatch_dashboard" "data_transfer" {
  dashboard_name = "data-transfer-costs"

  dashboard_body = jsonencode({
    widgets = [
      {
        type   = "metric"
        x      = 0
        y      = 0
        width  = 12
        height = 6
        properties = {
          metrics = [
            ["AWS/EC2", "NetworkOut", { stat = "Sum", period = 86400 }],
            ["AWS/EC2", "NetworkIn", { stat = "Sum", period = 86400 }]
          ]
          title  = "EC2 Network Traffic (Daily)"
          region = var.region
        }
      },
      {
        type   = "metric"
        x      = 12
        y      = 0
        width  = 12
        height = 6
        properties = {
          metrics = [
            ["AWS/NATGateway", "BytesOutToDestination", { stat = "Sum", period = 86400 }],
            ["AWS/NATGateway", "BytesOutToSource", { stat = "Sum", period = 86400 }]
          ]
          title  = "NAT Gateway Traffic (Daily)"
          region = var.region
        }
      }
    ]
  })
}
Enter fullscreen mode Exit fullscreen mode

⚑ Quick Audit: Where's Your Money Going?

# Check data transfer costs by service (last 30 days)
aws ce get-cost-and-usage \
  --time-period Start=$(date -u -d '30 days ago' +%Y-%m-%d),End=$(date -u +%Y-%m-%d) \
  --granularity MONTHLY \
  --metrics BlendedCost \
  --filter '{
    "Dimensions": {
      "Key": "USAGE_TYPE_GROUP",
      "Values": ["EC2: Data Transfer - Internet (Out)",
                  "EC2: Data Transfer - Region to Region (Out)",
                  "EC2: Data Transfer - Inter AZ"]
    }
  }' \
  --group-by Type=DIMENSION,Key=USAGE_TYPE_GROUP \
  --output table
Enter fullscreen mode Exit fullscreen mode

πŸ’‘ Quick Reference: What to Fix First

Problem Fix Savings Effort
S3/DynamoDB via NAT Gateway VPC Gateway Endpoints $0.135/GB β†’ Free 5 min
S3 direct to internet CloudFront in front $0.09/GB β†’ Free (1TB) 20 min
Public IP same-AZ traffic Use private IPs $0.02/GB β†’ Free 10 min
All traffic via NAT Gateway Interface Endpoints for AWS services $0.045/GB saved 15 min
Full bucket cross-region replication Filter by prefix 50-90% less transfer 10 min
Cross-AZ microservice chatter (non-prod) Pin to same AZ $0.02/GB β†’ Free 10 min

Start with VPC Gateway Endpoints. They're free, take 5 minutes, and eliminate the most expensive trap on this list. 🎯

πŸ“Š TL;DR

Same AZ, private IP:     FREE βœ… (always use this)
Same AZ, public IP:      $0.02  (why??)
Cross-AZ:                $0.02  (Multi-AZ tax)
Cross-Region:            $0.02  (replicate selectively)
To Internet:             $0.09  (use CloudFront instead)
Via NAT Gateway:         +$0.045 on top (use VPC Endpoints)
S3/DynamoDB via Endpoint: FREE βœ… (no-brainer)
S3 β†’ CloudFront:         FREE βœ… (origin fetch is free)
CloudFront β†’ Internet:   $0.085 + 1TB free (cheaper than S3 direct)
Enter fullscreen mode Exit fullscreen mode

Bottom line: Data transfer is the most confusing line item on your AWS bill β€” and that's by design. Print this cheat sheet, tape it to your monitor, and stop paying for traffic that should be free. πŸ–¨οΈ


I bet you don't have VPC Gateway Endpoints for S3 yet. Go deploy them right now β€” it's 5 lines of Terraform and saves $0.135 on every GB. I'll wait. πŸ˜€

Found this helpful? Follow for more AWS cost optimization with Terraform! πŸ’¬

Top comments (2)

Collapse
 
khadijah profile image
Khadijah (Dana Ordalina)

nice post

Collapse
 
suhas_mallesh profile image
Suhas Mallesh

Thank you :)