Architecting Around Egress: The Patterns Teams Use When the Bandwidth Bill Exceeds the Compute

Sources

On 12 January 2027, Article 29 of the EU Data Act makes it illegal for a cloud provider to charge you for leaving — switching charges, explicitly including data egress, go to zero. The hyperscalers did not wait: Google has zero-rated qualifying multicloud traffic for EU and UK customers since 10 September 2025, Microsoft credits EEA/EFTA/UK in-parallel transfers at cost on request, and AWS's move-out waiver — which its own blog notes requires a support ticket and gives non-EU customers 180 days to complete the move (updated 10/8/26) — is available worldwide. Regulators have decided egress is an anti-competitive moat, and the pricing is finally bending.

Here is the uncomfortable baseline, though, straight from AWS's own announcement: over 90 percent of AWS customers already incur no data-transfer-out expense at all, because 100 GB/month to the internet is free (aggregated across every service and region except China and GovCloud) and CloudFront throws in 1 TB/month. If you are in that 90 percent, this guide is a reference for the day your traffic changes shape. If you are in the other 10 percent — media platforms, AI inference endpoints, data lake egress, replicated databases, a CDN that stopped caching — your egress bill is not a pricing problem. It is a topology problem, and topology is reversible if you know which meter each path is attached to.

So this guide does two things. First, it lays out the verified meter map: every rate that can attach to a byte leaving a workload, pulled from the live pricing pages and the AWS Price List API offer files on 2026-11-07 — not from blog posts that round the tiers wrong. Second, it walks the six patterns that actually move the number: private endpoints, AZ-affinity, CDN fronting, zero-egress storage, replicate-once-read-local, and the dedicated-switching lanes. Each comes with complete manifests, the honest math (Decimal-computed, allowances applied), and the failure modes we have watched undo the savings. If you want the east-west half of this story — hub data-processing fees and the TGW/NCC tax — that is covered in Hub-and-Spoke vs. Mesh Cloud Networking; this guide is the north-south half.

The meter map: what actually bills as egress

Most "surprising" egress bills come from teams modeling one rate — the internet-egress tier table — while the traffic rides three or four meters at once. A byte leaving an EC2 instance in us-east-1 can attach to any of the following, and they stack where the paths overlap:

  SOURCE (workload, AZ-a)                     DESTINATION                 METER (USD)
  ------------------------------------------  --------------------------  -------------------------------
  private IP, same AZ                         same-AZ workload             $0 (no meter)
  public or Elastic IPv4 address               ANY destination             $0.01/GB, EACH direction
  private IP, cross-AZ                         AZ-b workload               $0.01/GB in + $0.01/GB out
  any, via NAT Gateway                         internet                    $0.045/GB processing + internet tiers
  any, via NAT Gateway                         S3/DynamoDB (same region)   $0.045/GB processing, $0 transfer
  any, via gateway endpoint                    S3/DynamoDB                 $0 (no hourly, no processing)
  any, via interface endpoint                  supported service           $0.01/GB (first PB) + $0.01/AZ-hr
  cross-region                                 us-east-2 (Ohio)            $0.01/GB
  cross-region                                 eu-central-1 (Frankfurt)    $0.02/GB
  to internet                                  users/clients               100 GB free, then $0.09/GB tiered
  via Transit Gateway hub                      any attachment               $0.02/GB processing (+ $0.05/hr attach)
  S3 origin, fetched by CloudFront             CloudFront                   $0 (S3 side; CF origin fetch also $0 in us-east-1)
  CloudFront edge, served to viewers           internet                     1 TB free, then $0.085/GB tiered

Three structural facts from that table decide most of your bill. First, the public-IP tax: traffic that enters or leaves via a public or Elastic IPv4 address is billed $0.01/GB in each direction even when both ends sit in the same Availability Zone — a service that calls itself through its public DNS name pays a meter that private DNS never touches. Second, NAT Gateway data processing applies regardless of source or destination — the pricing page's own words — which is why S3 traffic routed through a NAT gateway costs $0.045/GB in processing for $0.00 of "transfer". Third, the meters are per-direction: the cross-AZ round trip is $0.02/GB, not $0.01/GB, and every chatty microservice pair you spread across AZs pays it twice, monthly, forever.

One units trap before the tables: AWS bills TB as 1,024 GB (the Direct Connect page states "1 TB = 1,024 GB" outright), Azure's bandwidth page states "1 TB = 1,000 GB", and Google bills in binary GiB by name. Any cross-cloud comparison you see that ignores this is off by 2.4% before it starts.

The verified price floor, November 2026

Internet egress tiers, from the live pricing pages and the Price List API offer files. The AWS numbers below are from the AWSDataTransfer offer file dated 2026-11-03 — the same files the bill is computed from, with the tier boundaries intact:

Tier (monthly)AWS us-east-1Azure Zone 1 (MGN routing)Google Cloud Premium (us-central1)Cloudflare R2
Free allowance100 GB (account-global)100 GB (per billing account)Premium: none. Standard: 200 GiB (account-global)Egress: unlimited, $0
First 10 TB$0.090/GB$0.087/GB$0.12/GiB (first 1 TiB), $0.11/GiB (to 10 TiB)$0 egress. Storage $0.015/GB-mo. Class B reads $0.36/M.
Next 40 TB$0.085/GB$0.083/GB$0.085/GiB above 10 TiB
Next 100 TB$0.070/GB$0.070/GB
Above 150 TB$0.050/GB$0.050/GB (to 350 TB)Standard tier: $0.085/$0.065/$0.045/GiB after the free 200 GiB

Azure's Zone 2 (Asia ex-China, Australia, MEA) runs $0.12/$0.085/$0.082/$0.08 and Zone 3 (South America) $0.181/$0.175/$0.17/$0.16 — geography matters more than negotiation at these tiers. The same page lists the east-west rates: cross-Availability-Zone transfer is free in Azure (Microsoft eliminated the fee in June 2024, for private and public IPs alike), inter-region within Europe is $0.02/GB, and intercontinental from North America or Europe is $0.05/GB. Google charges $0.01/GiB between zones in a region (intra-zone on private IPs is free) and its Standard tier — which rides "the public internet" with, in Google's own words, "a lower quality of service" — undercuts Premium by ~24% at 100 TB. AWS is the only one of the three still charging both directions cross-AZ.

The east-west and private-path meters, same verification date:

MeterAWSAzureGoogle Cloud
Cross-zone, per direction$0.01/GB (public/Elastic IP: also $0.01/GB each way, same AZ included)$0 (since June 2024)$0.01/GiB (external IPv4 always counts as inter-zone)
Inter-region$0.01/GB to Ohio; $0.02/GB to Frankfurt/Tokyo/Melbourne; $0.086/GB Mumbai→US$0.02/GB within Europe; $0.08/GB within Asia/Oceania/MEAvaries by region pair (premium backbone)
NAT egress processing$0.045/GB + $0.045/hr per gateway (Regional NAT: per AZ)NAT Gateway per-hour + per-GB (metered)Cloud NAT per-VM-hour + per-GB
Private endpoint data processingGateway (S3/DynamoDB): $0. Interface (PrivateLink): $0.01/GB first PB + $0.01/hr per AZPrivate Endpoints: per-hour + per-GBPrivate Service Connect: per-endpoint + per-GB
Managed hub processingTGW $0.02/GB + $0.05/hr per attachmentVWAN hub $0.02/GB processedNCC $0.02/GiB through-hub (ADN)
Dedicated circuit outboundDirect Connect $0.02/GB (inbound $0; 1 Gbps port $0.33/hr)ExpressRoute metered Zone 1 $0.025/GB (inbound free; Unlimited plan available)Dedicated Interconnect $0.02/GiB (North America)

Now the only comparison that matters — the same 100 TB/month of internet delivery through each lane, computed with Decimal against those exact tiers (free allowances applied, 1,024 GB/TB for AWS, 1,000 GB/TB for Azure, GiB for Google; object size 1 MiB where request fees apply):

LANE                                            MONTHLY (USD)   VS BASELINE
---------------------------------------------------------------------------
AWS direct egress (EC2/S3 tiers)                 $7,980.20       baseline
Azure Zone 1 (Premium Global Network)           $7,683.00       -3.7%
Google Premium tier (us-central1)               $8,970.24       +12.4%
Google Standard tier (public internet path)     $6,843.80       -14.3%  (QoS caveat)
CloudFront fronting S3, 0% cache hit             $7,262.62       -9.0%
CloudFront fronting S3, 70% cache hit             $2,458.34       -69.2%
Direct Connect to your own facility            $2,288.90       -71.3%  (egress $2,048 + 1Gbps port $240.90)
Cloudflare R2 as origin (S3-compatible)          $0.00  egress  -100%   (storage + ops still bill)

Read the CloudFront-0%-cache line carefully, because it kills a myth: with a cache hit ratio of zero, the CDN saves you only ~9% — the tier delta ($0.085 vs $0.09) plus the 1 TB free allowance minus request fees. The CDN is not an egress discount; it is a caching play that happens to bill less per uncached byte. Everything else on this list is a topology change, and the rest of this guide is the mechanics of each.

Pattern 1 — Take private traffic off the metered path

The most common self-inflicted egress bill in AWS is S3 traffic that routes through a NAT gateway because the subnets are private and nobody created a gateway endpoint. The meter is pure processing: $0.045 for every gigabyte, added by your own architecture, to traffic that AWS would otherwise carry for free. The VPC pricing page documents both halves — the NAT gateway data-processing charge "regardless of the traffic's source or destination", and, for gateway endpoints, "there are no data processing or hourly charges."

# S3 gateway endpoint: route-table based, free, no hourly charge.
# References the community terraform-aws-modules/vpc for the existing VPC.
resource "aws_vpc_endpoint" "s3" {
  vpc_id            = module.vpc.vpc_id
  service_name      = "com.amazonaws.us-east-1.s3"
  vpc_endpoint_type = "Gateway"
  route_table_ids   = module.vpc.private_route_table_ids

  policy = jsonencode({
    Version = "2012-10-17",
    Statement = [
      {
        Sid       = "AllowMediaBucketOnly"
        Effect    = "Allow"
        Principal = { AWS = "*" }
        Action    = ["s3:GetObject", "s3:PutObject", "s3:ListBucket"]
        Resource  = [
          "arn:aws:s3:::platform-monkey-media",
          "arn:aws:s3:::platform-monkey-media/*"
        ]
      }
    ]
  })
}

# Interface endpoints for ECR: container pulls stop paying NAT processing
# and stop depending on a NAT gateway that has one foot in a single AZ.
resource "aws_security_group" "endpoint" {
  name_prefix = "vpc-endpoint-"
  vpc_id      = module.vpc.vpc_id
  ingress {
    description = "HTTPS from VPC CIDR"
    from_port   = 443
    to_port     = 443
    protocol    = "tcp"
    cidr_blocks = [module.vpc.vpc_cidr_block]
  }
  egress {
    description = "HTTPS out"
    from_port   = 443
    to_port     = 443
    protocol    = "tcp"
    cidr_blocks = ["0.0.0.0/0"]
  }
}

resource "aws_vpc_endpoint" "ecr_dkr" {
  vpc_id              = module.vpc.vpc_id
  service_name        = "com.amazonaws.us-east-1.ecr.dkr"
  vpc_endpoint_type   = "Interface"
  subnet_ids          = module.vpc.private_subnets
  security_group_ids  = [aws_security_group.endpoint.id]
  private_dns_enabled = true
}

resource "aws_vpc_endpoint" "ecr_api" {
  vpc_id              = module.vpc.vpc_id
  service_name        = "com.amazonaws.us-east-1.ecr.api"
  vpc_endpoint_type   = "Interface"
  subnet_ids          = module.vpc.private_subnets
  security_group_ids  = [aws_security_group.endpoint.id]
  private_dns_enabled = true
}

The economics, computed: a pipeline that pushes 500 GB/day to S3 through a NAT gateway pays $0.045 × 500 × 30 = $675/month in processing for traffic whose transfer cost is zero. At 2 TB/day it is $2,764.80/month. The gateway endpoint makes it $0 — it is a prefix list in your route tables, it survives AZ failure better than a single-homed NAT gateway, and the endpoint policy above also fixes the other silent problem: an open S3 endpoint is an exfiltration lane to any bucket in the account. Scope the policy to the buckets the subnets are supposed to touch.

The honest limit: gateway endpoints exist for exactly two services (S3 and DynamoDB). Everything else needs an interface endpoint, which meters $0.01/GB processed (first PB, then $0.006 to 5 PB, then $0.004) plus $0.01 per hour per endpoint ENI per AZ. That is 4.5× cheaper than NAT processing per gigabyte, but it is not free, and the hourly floor multiplies across AZs and services — an EKS cluster with endpoints for ECR, STS, SSM, CloudWatch, Secrets Manager, and KMS in three AZs carries ~$216/month in endpoint-hours before a single byte moves. Cheap, not invisible.

Pattern 2 — Kill the cross-AZ tax, and the public-IP trap under it

AWS bills $0.01/GB in each direction for traffic that crosses Availability Zones, and the same rate again for anything that transits a public or Elastic IPv4 address — including same-AZ traffic. Azure charges nothing cross-AZ since June 2024; Google charges $0.01/GiB. On AWS, this is the tax nobody budgets: a request-reply pair exchanging 500 GB/day across an AZ boundary costs $300/month; a replication or feature-store pair moving 5 TB/day pays $3,072/month — in pure meter, for traffic that never leaves the building.

Before co-locating anything, find the actual chatty pairs — do not guess, audit. Enable VPC Flow Logs (v5+ format) to Athena and rank interfaces by bytes; join against your subnet-to-AZ mapping to label the cross-AZ rows:

SELECT
  interface_id,
  sum(bytes) / 1024.0 / 1024.0 / 1024.0 AS gib_egress_30d,
  round(sum(bytes) * 0.02 / 1000000000.0, 2) AS usd_if_round_trip,  -- $0.01/GB x 2 directions
  count(distinct pkt_dstaddr) AS distinct_destinations
FROM "athena_database"."vpc_flow_logs"
WHERE day BETWEEN '2026/10/08' AND '2026/11/07'
  AND flow_direction = 'EGRESS'
  AND action = 'ACCEPT'
  AND pkt_dstaddr NOT LIKE '10.%'           -- private RFC1918 peers: label with your CIDR map
GROUP BY interface_id
ORDER BY gib_egress_30d DESC
LIMIT 25;

Then apply the cheapest fix in the whole guide: schedule the two pods in the same zone. The Kubernetes form, using a preferred (soft) affinity so you keep AZ spread when the cluster is tight:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: frame-encoder
spec:
  replicas: 12
  selector:
    matchLabels:
      app: frame-encoder
  template:
    metadata:
      labels:
        app: frame-encoder
    spec:
      affinity:
        podAffinity:
          preferredDuringSchedulingIgnoredDuringExecution:
            - weight: 80
              podAffinityTerm:
                labelSelector:
                  matchLabels:
                    app: ingest-gateway
                topologyKey: topology.kubernetes.io/zone
      topologySpreadConstraints:
        - maxSkew: 2
          topologyKey: kubernetes.io/hostname
          whenUnsatisfiable: ScheduleAnyway
          labelSelector:
            matchLabels:
              app: frame-encoder
      containers:
        - name: frame-encoder
          image: registry.internal/platform/frame-encoder:1.42.0
          resources:
            requests: { cpu: "2", memory: "4Gi" }
            limits: { cpu: "2", memory: "4Gi" }

Two traps sit under this pattern. The public-IP trap: a workload that reaches another workload through its public DNS name (NLB with client-IP preservation, cross-account APIs, a service mesh that dials public endpoints) pays the $0.01/GB-each-direction meter even inside a single AZ — the fix is a private DNS record or an interface endpoint, not co-location. And the availability trade: pinning replicas together in one zone converts a zone outage from "degraded capacity" to "incident", which is why the manifest uses preferredDuringScheduling rather than requiredDuringScheduling. Reserve hard pinning for pairs that are already single-AZ by design (a stateful primary and its sync worker, say) — and re-read the blast-radius trade-offs in the hub-and-spoke guide before you apply it fleet-wide.

Pattern 3 — Front it with a CDN, and know what that actually buys

The CDN pattern is the only one that reduces the bytes you serve, not just the rate on them. The verified AWS mechanics: S3→CloudFront transfer is $0.00 (S3 pricing, free list), origin fetch from an S3 origin in us-east-1 is $0.00 per GB in the CloudFront offer file, CloudFront serves the first 1 TB/month free, and the US egress tiers run $0.085/$0.080/$0.060/$0.040/GB down to $0.020 beyond 5 PB — with HTTPS requests at $0.0100 per 10,000. The math above already showed the honest split: at 0% cache hit you save ~9% on the rate; at a 70% hit ratio the same 100 TB costs $2,458 instead of $7,980. Cache hit ratio is the entire ballgame.

resource "aws_cloudfront_origin_access_control" "media" {
  name                              = "oac-media-bucket"
  description                       = "OAC for the S3 media origin"
  origin_access_control_origin_type = "s3"
  signing_behavior                  = "always"
  signing_protocol                  = "sigv4"
}

resource "aws_cloudfront_distribution" "media" {
  enabled         = true
  comment         = "media egress lane"
  price_class     = "PriceClass_100"   # NA+EU edges; widen when the audience does
  is_ipv6_enabled = true
  http_version    = "http2"

  origin {
    domain_name              = aws_s3_bucket.media.bucket_regional_domain_name
    origin_id                = "s3-media"
    origin_access_control_id = aws_cloudfront_origin_access_control.media.id
    s3_origin_config {
      origin_access_identity = ""     # OAC replaces the legacy origin access identity
    }
  }

  default_cache_behavior {
    target_origin_id       = "s3-media"
    viewer_protocol_policy = "redirect-to-https"
    allowed_methods        = ["GET", "HEAD"]
    cached_methods         = ["GET", "HEAD"]
    cache_policy_id        = "658327ea-f89d-4fab-a63d-7e88639e58f6"  # AWS-managed CachingOptimized
    compress               = true
  }

  restrictions {
    geo_restriction {
      restriction_type = "none"
    }
  }

  viewer_certificate {
    cloudfront_default_certificate = true
  }
}

# Bucket side: OAC principal, scoped to this distribution's ARN.
data "aws_caller_identity" "current" {}

resource "aws_s3_bucket_policy" "media_cdn" {
  bucket = aws_s3_bucket.media.id
  policy = jsonencode({
    Version = "2012-10-17",
    Statement = [
      {
        Sid       = "AllowCloudFrontOACRead"
        Effect    = "Allow"
        Principal = { Service = "cloudfront.amazonaws.com" }
        Action    = "s3:GetObject"
        Resource  = "arn:aws:s3:::platform-monkey-media/*"
        Condition = {
          StringEquals = {
            "AWS:SourceArn" = "arn:aws:cloudfront::${data.aws_caller_identity.current.account_id}:distribution/${aws_cloudfront_distribution.media.id}"
          }
        }
      }
    ]
  })
}

Operational notes that decide whether this pays. Origin Shield ($0.0075 per 10,000 requests in us-east-1, data transfer to/from the origin free) collapses multi-edge origin fetches into one — worth it the moment your fill rate matters, which for a media or model-artifact bucket is immediately. Custom origins pay their own meter: data transfer out to a non-S3 US origin is $0.02/GB, so fronting your own ALB/EC2 origin still bills that path even at 0% hit — and an on-prem origin bills it on your WAN side too. Never front with a "CDN" and disable caching: a passthrough CloudFront against a dynamic API saves 9% at best and adds a TLS hop. The pattern applies when the content is cacheable for even hours (model artifacts, container layers, media segments, ML feature snapshots); for genuinely uncachable responses, skip straight to Patterns 4–6. Azure's equivalent lane is Azure Front Door / Azure CDN from Microsoft, and Azure's at-cost EEA/UK transfer program explicitly excludes CDN delivery — the two patterns do not compose there.

Pattern 4 — Move the data's gravity center to zero-egress storage

Cloudflare R2's pitch is one line: no charges for egress bandwidth, any storage class, S3-compatible API, storage at $0.015/GB-month (Standard) with Class A writes $4.50/million and Class B reads $0.36/million. For a 50 TB store serving 100 TB/month, the computed comparison is $1,177.60 of S3 storage plus $7,980.20 of S3 egress versus $768.00 of R2 storage, $37.75 of Class B ops (at 1 MiB objects) and $0 egress — roughly a 93% reduction for read-dominated workloads, before you price the request fees. Backblaze B2 and Wasabi play the same zero-egress game in the same niche; R2 is the one with the S3 API surface your tooling already speaks.

resource "cloudflare_r2_bucket" "media" {
  account_id    = var.cloudflare_account_id
  name          = "platform-monkey-media"
  location_hint = "apac"     # placement hint; buckets are global from the client's view
}
apiVersion: batch/v1
kind: CronJob
metadata:
  name: s3-to-r2-sync
spec:
  schedule: "0 */6 * * *"
  concurrencyPolicy: Forbid
  jobTemplate:
    spec:
      backoffLimit: 2
      template:
        spec:
          restartPolicy: OnFailure
          containers:
            - name: rclone
              image: rclone/rclone:1.70.3
              command:
                - rclone
                - sync
                - awss3:platform-monkey-media
                - r2:platform-monkey-media
                - --transfers=32
                - --checkers=16
                - --s3-no-check-bucket
              env:
                - name: RCLONE_CONFIG_AWSS3_TYPE
                  value: "s3"
                - name: RCLONE_CONFIG_AWSS3_REGION
                  value: "us-east-1"
                - name: RCLONE_CONFIG_AWSS3_ACCESS_KEY_ID
                  valueFrom:
                    secretKeyRef: { name: aws-s3-readonly, key: access-key-id }
                - name: RCLONE_CONFIG_AWSS3_SECRET_ACCESS_KEY
                  valueFrom:
                    secretKeyRef: { name: aws-s3-readonly, key: secret-access-key }
                - name: RCLONE_CONFIG_R2_TYPE
                  value: "s3"
                - name: RCLONE_CONFIG_R2_ENDPOINT
                  value: "https://ACCOUNT_ID.r2.cloudflarestorage.com"
                - name: RCLONE_CONFIG_R2_ACCESS_KEY_ID
                  valueFrom:
                    secretKeyRef: { name: r2-creds, key: access-key-id }
                - name: RCLONE_CONFIG_R2_SECRET_ACCESS_KEY
                  valueFrom:
                    secretKeyRef: { name: r2-creds, key: secret-access-key }

For one-shot bulk moves, S3 Batch Operations is the in-platform alternative — the offer file meters it at $0.25 per job plus $1.00 per million objects — and Cloudflare documents its own migration tooling (Super Slurper for bulk copies, Sippy for incremental/live cutover) on the R2 pricing page. Now the honesty, because this pattern has two fees people miss. First, the migration itself egresses: copying 50 TB out of S3 to the internet bills ~$4,500 of AWS DTO unless it qualifies as a move-out (all data off a service) under the AWS waiver — which is exactly the process Pattern 6 describes, and the two compose: request the free-DTO rates, move to R2, close the S3 side. Second, ops fees bite chatty workloads: Class A writes at $4.50/million mean a bucket receiving 1 billion small uploads/month pays $4,500 in ops before storage; multipart uploads multiply request counts; and the 10 GB free tier covers Standard class only. Zero-egress storage is a read-heavy, large-object play — at small objects and high churn it can cost more than the egress it replaces.

Pattern 5 — Replicate once, read local

When readers are global and writers are single-homed, every-request inter-region traffic is the expensive design; replicate-once is the cheap one. The verified rates: us-east-1→us-east-2 is $0.01/GB, us-east-1→Frankfurt is $0.02/GB, billed on the replication path. The computed trade for a 10 TB dataset read 30 TB/month from a second region: cross-region replication costs $204.80 once (plus the destination storage), while serving those reads remotely costs $614.40 every month — and adds cross-region latency to every read. Three cycles to break even; the pattern pays for itself within a quarter and then compounds.

resource "aws_s3_bucket_versioning" "media" {
  bucket = aws_s3_bucket.media.id
  versioning_configuration {
    status = "Enabled"          # CRR refuses to work without versioning
  }
}

resource "aws_iam_role" "crr" {
  name = "media-crr-role"
  assume_role_policy = jsonencode({
    Version = "2012-10-17",
    Statement = [
      {
        Effect    = "Allow"
        Action    = ["sts:AssumeRole"]
        Principal = { Service = "s3.amazonaws.com" }
      }
    ]
  })
}

resource "aws_iam_role_policy" "crr" {
  name = "media-crr-policy"
  role = aws_iam_role.crr.id
  policy = jsonencode({
    Version = "2012-10-17",
    Statement = [
      {
        Effect = "Allow",
        Action = [
          "s3:GetReplicationConfiguration",
          "s3:ListBucket",
          "s3:GetObjectVersionForReplication",
          "s3:GetObjectVersionAcl",
          "s3:GetObjectVersionTagging"
        ],
        Resource = [aws_s3_bucket.media.arn, "arn:aws:s3:::platform-monkey-media/*"]
      },
      {
        Effect = "Allow",
        Action = ["s3:ReplicateObject", "s3:ReplicateDelete", "s3:ReplicateTags"],
        Resource = "arn:aws:s3:::platform-monkey-media-usw2/*"
      }
    ]
  })
}

resource "aws_s3_bucket_replication_configuration" "media" {
  depends_on = [aws_s3_bucket_versioning.media]
  role       = aws_iam_role.crr.arn
  bucket     = aws_s3_bucket.media.id

  rule {
    id     = "replicate-to-usw2"
    status = "Enabled"
    delete_marker_replication {
      status = "Disabled"       # deliberate: keep delete markers local
    }
    filter {}
    destination {
      bucket        = "arn:aws:s3:::platform-monkey-media-usw2"
      storage_class = "STANDARD_IA"
    }
  }
}

The operational caveats: CRR only replicates objects written after enabling the rule (backfill needs S3 Batch Operations re-copying existing versions), replication metrics lag, and delete-marker semantics are a design decision, not a default — the rule above deliberately keeps deletes local so a fat-fingered delete in us-east-2 does not propagate. For databases the same shape applies (RDS cross-region read replicas, Cosmos geo-replication, Cloud Spanner dual-region), with the identical logic: pay the replication meter once per write, never per read. The multi-region failure-mode analysis that should gate this decision — quorum placement, split-brain, failover — is covered in Multi-Region Active-Active.

Pattern 6 — The switching lanes: dedicated circuits and the Data Act endgame

Steady hybrid traffic has a rate card of its own, and it is dramatically cheaper than internet egress: Direct Connect bills $0.00 inbound and $0.02/GB outbound (region- and DX-location-dependent; the 1 Gbps dedicated port is $0.33/hour), ExpressRoute metered bills inbound free and Zone 1 outbound at $0.025/GB (Zone 2 $0.05, Zone 3 $0.14, Zone 4 $0.10) with an Unlimited plan at a fixed port price, and Dedicated Interconnect egress for North America is $0.02/GiB. The computed 100 TB figure — $2,288.90 including the 1 Gbps port — is why a colo with flat-rate transit is still the cheapest CDN origin on earth for steady, predictable egress.

The regulatory lane is the one with a deadline. Article 29 of the EU Data Act already caps switching charges at cost (since 11 January 2024) and bans them outright — including egress for the switch — from 12 January 2027. Since 12 September 2025 the rules bind existing contracts and non-EU providers serving EU customers. What remains chargeable after January is the carve-out: in-parallel multi-cloud use (Article 34(2)) passes through at cost, permanently. The three providers' operational reality today:

ProviderProcedureWindowCatches
AWS (move-out waiver)Support ticket, per account/service; credits applied after approvalNon-EU: 180 days to complete the move (updated 10/8/26); EU: 30-calendar-day transition, extendable, per the EU Data Act AddendumRequest "at least two months before" a service-level switch; CloudFront, DX, Snow Family and Global Accelerator traffic excluded; in-parallel use only gets reduced rates on request
Azure (at-cost program)Support request with subscription ID, destination ASN, and percentage of traffic per endpointCredit on next bill; automatic for future cyclesEEA/EFTA/UK billing address only; ISP routing preference only (UK: MGN from UK datacenters); same-organization destinations only; CDN delivery excluded; Azure "might make changes regarding the egress credit policy"
Google (Data Transfer Essentials)Opt-in; qualifying multicloud traffic metered separately at zeroLive since 10 September 2025EU and UK only; same organization across providers; 21 supported Google services; non-qualifying traffic bills at normal Network Service Tier rates

Two dates matter if you are planning an EU-anchored exit. The Article 29(1) prohibition lands 12 January 2027, and AWS's process requires the switching request roughly two months ahead — which means the practical filing deadline for a January switch is mid-November 2026, not January. And note what the Data Act does not do: it does not ban egress for steady-state operations. If your architecture keeps two providers in parallel, Article 34(2) keeps that meter alive forever; the zero-cost lanes for that shape are Google's Data Transfer Essentials and Azure's at-cost program. That is the entire strategic argument of this guide in one paragraph: switching is becoming free; staying multi-cloud is not.

The runbook: find your bytes before you re-architect

Every pattern below is wasted if you cannot name the meter line-items you are attacking. Thirty minutes of billing queries beats a quarter of re-platforming. The meter names, grouped:

aws ce get-cost-and-usage \
  --time-period Start=2026-10-01,End=2026-11-01 \
  --granularity MONTHLY \
  --metrics "UnblendedCost" "UsageQuantity" \
  --group-by Type=DIMENSION Key=USAGE_TYPE \
  --query 'ResultsByTime[0].Groups[?starts_with(Keys[0], `DataTransfer`) || contains(Keys[0], `NatGateway`) || contains(Keys[0], `DataTransfer-Out`)].{usage:Keys[0], cost:Metrics.UnblendedCost.Amount}' \
  --output table

# The lines you are hunting:
#   USE1-DataTransfer-Out-Bytes   internet egress (the tiered meter)
#   DataTransfer-Regional-Bytes    cross-AZ (billed each direction)
#   USE1-NatGateway-Bytes          NAT processing ($0.045/GB of avoidable tax)
#   USE1-DataTransfer-Out-Bytes   per-region egress across service accounts
SELECT
  sku.description AS sku,
  SUM(cost) AS cost_30d,
  SUM(usage.amount) AS units
FROM `billing-admin.gcp_billing_export.resource_v1_01AB23CD45EF`
WHERE usage_start_time BETWEEN TIMESTAMP('2026-10-08') AND TIMESTAMP('2026-11-07')
  AND (sku.description LIKE '%Network Internet Egress%'
       OR sku.description LIKE '%Interzone%'
       OR sku.description LIKE '%Network Egress via Interconnect%')
GROUP BY sku.description
ORDER BY cost_30d DESC;

On Azure, Cost Management filters on the Bandwidth meter category; the line items to rank are "Data Transfer Out", inter-region transfer, and the NAT Gateway processing meter. Watch for routing-preference meters appearing (a public IP created with ISP routing bills its own, cheaper egress line) and reconcile them against the at-cost program's ISP-path requirement. Add a monitoring rule for the meters after you change topology: CloudWatch's NAT Gateway metrics (NatGatewayBytesOutToDestination) catch endpoints that silently re-route through the gateway when someone deletes a route-table entry, and a Cost anomaly detector on the DataTransfer line catches a CDN cache-hit collapse within a day instead of at invoice time.

Production realities: where each pattern breaks

Who should skip this entire guide

The decision table

Your meter lineThe patternVerified saving at reference scale
S3/DynamoDB traffic via NAT gatewayGateway endpoint (free, route-table)$675/mo at 500 GB/day; $2,764.80/mo at 2 TB/day
Cross-AZ chatter between paired servicesFlow-log audit + soft AZ-affinity$300/mo per 500 GB/day pair; the public-IP variant is the same meter
Cacheable internet delivery > 1 TB/monthCloudFront/Front Door with OAC + Origin Shield−69% at 100 TB with 70% hit; only −9% at 0% hit
Egress-dominated object storage, read-heavyR2/B2/Wasabi zero-egress origin≈−93% (storage+ops+egress) for 50 TB store, 100 TB/mo reads
Global readers of a single-writer datasetCRR replicate-once, read local$204.80 once vs $614.40/mo remote serving (10 TB, 30 TB/mo)
Steady hybrid egress to your facilityDirect Connect / ExpressRoute / Interconnect$2,288.90/mo at 100 TB incl. port, vs $7,980.20 internet
Full or service-level cloud exitData Act waiver lanes (support ticket)100% of switch egress, from 12 Jan 2027 by right, earlier by request
EU/UK steady multi-cloud trafficGoogle Data Transfer Essentials / Azure at-costQualifying traffic at $0 / at cost, per the program tables above

The pattern that unifies all of them: egress bills follow topology, and topology is the one cloud bill you can actually change. Compute prices move a few percent a year on someone else's roadmap; whether a byte rides a NAT gateway, crosses an AZ boundary, exits through an edge cache, or replicates once instead of per-request is your architecture. The meters above are the verified November 2026 rates — re-check the tier tables quarterly, because the one thing this market has proven is that the egress line item does move, and lately it has been moving down.