Architecting Around Egress: The Patterns Teams Use When the Bandwidth Bill Exceeds the Compute
Sources
- Amazon EC2 On-Demand pricing — data transfer (verified 2026-11-07)
- AWS Price List API — AWSDataTransfer offer, us-east-1 (version 2026-11-03)
- Amazon CloudFront pricing (verified 2026-11-07)
- AWS Price List API — AmazonCloudFront offer (version 2026-10-19)
- Amazon VPC pricing — NAT Gateway, Regional NAT, gateway endpoints (verified 2026-11-07)
- AWS PrivateLink pricing — interface endpoint data processing tiers (verified 2026-11-07)
- Amazon S3 pricing (verified 2026-11-07)
- AWS Price List API — AmazonS3 offer, us-east-1 (version 2026-11-04)
- AWS Direct Connect pricing (verified 2026-11-07)
- AWS News Blog — Free data transfer out to internet when moving out of AWS (updated 10/8/26)
- Azure bandwidth pricing (verified 2026-11-07)
- Azure ExpressRoute pricing (verified 2026-11-07)
- Azure data transfer fees — at-cost transfer for EEA/EFTA/UK (Microsoft Learn)
- Routing preference in Azure — MGN vs ISP egress (Microsoft Learn)
- Google Cloud VPC network pricing (verified 2026-11-07)
- Google Cloud Interconnect pricing (verified 2026-11-07)
- Cloudflare R2 pricing — zero egress, storage and operation fees (verified 2026-11-07)
- Reuters — Google scraps some cloud data transfer fees in EU and UK (2025-09-10)
- Bundesnetzagentur — EU Data Act factsheet: switching charges phased out by 12 January 2027
- Data Act Kit — Article 29 deadline and the in-parallel-use carve-out (Article 34)
- InfoQ — Azure eliminates inter-Availability Zone data transfer fees (June 2024)
- WZ-IT — Data Act 2027: switching charges and egress, provider-by-provider procedures
- Amazon CloudFront developer guide — managed cache policies (CachingOptimized ID)
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 tieredThree 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-1 | Azure Zone 1 (MGN routing) | Google Cloud Premium (us-central1) | Cloudflare R2 |
|---|---|---|---|---|
| Free allowance | 100 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:
| Meter | AWS | Azure | Google 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/MEA | varies 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 processing | Gateway (S3/DynamoDB): $0. Interface (PrivateLink): $0.01/GB first PB + $0.01/hr per AZ | Private Endpoints: per-hour + per-GB | Private Service Connect: per-endpoint + per-GB |
| Managed hub processing | TGW $0.02/GB + $0.05/hr per attachment | VWAN hub $0.02/GB processed | NCC $0.02/GiB through-hub (ADN) |
| Dedicated circuit outbound | Direct 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:
| Provider | Procedure | Window | Catches |
|---|---|---|---|
| AWS (move-out waiver) | Support ticket, per account/service; credits applied after approval | Non-EU: 180 days to complete the move (updated 10/8/26); EU: 30-calendar-day transition, extendable, per the EU Data Act Addendum | Request "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 endpoint | Credit on next bill; automatic for future cycles | EEA/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 zero | Live since 10 September 2025 | EU 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 accountsSELECT
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
- Gateway endpoints are route-table hygiene, not a firewall. The prefix list appears in the route table; anything that re-creates route tables (an EKS VPC-CNI upgrade, a Terraform apply that reorders
route_table_ids) can drop it silently, and your S3 traffic is back on the NAT gateway at $0.045/GB with no error, no alert — just a bigger bill. Alert onNatGatewayBytesOutToDestinationtrending against zero for S3 CIDRs. - AZ-affinity converts zone failures into incidents. Preferred affinity survives; required affinity does not. The moment a pair is hard-pinned to one zone, your "three-AZ" deployment is a two-AZ deployment with extra steps, and the cross-AZ meter you removed was buying blast-radius containment. The blast-radius analysis applies to zones, not just networks.
- CDN hit ratios drift, and the drift is invisible until the invoice. An origin change that strips cache headers (new API gateway, someone "simplifying" the build), a surge of unique query strings, a signed-URL change — any of these can take a 70% hit ratio to 20% and re-add ~$5,000/month at the 100 TB scale while every dashboard stays green. Alert on CloudFront cache hit rate as an SLO, not a vanity metric.
- Zero-egress storage is a lock-in inversion, not lock-in freedom. Moving in is cheap (ingress is free everywhere); moving out is free from R2's side but bills at whatever the source charges. R2's Class A/B ops fees are the actual meter for small-object workloads, multipart uploads multiply them, and the 10 GB free tier applies to Standard class only. Model ops fees before signing up, not after the invoice.
- Interface endpoints have an hourly floor that multiplies. $0.01/hr per endpoint ENI per AZ sounds like nothing; a 3-AZ EKS cluster with six endpoints carries ~$216/month before traffic. Gateway endpoints first, interface endpoints for the services that actually carry NAT-routed bytes, and remember PrivateLink's per-GB processing is per endpoint owner — cross-account sharing bills the consumer's side.
- The GCP Standard tier is a QoS decision. The 24% discount is real and so is the "lower quality of service" — traffic rides the public internet, and the 200 GiB free allowance is account-global, not per project, so a single hot project can consume it invisibly. Same class of caveat as Azure's ISP routing preference: cheaper path, thinner guarantees, and PIPs cannot change routing preference after creation or attach to NAT gateway resources.
- The Data Act lanes are support tickets, not infrastructure. Every program above is opt-in, request-driven, and explicitly revisable ("Azure might make changes regarding the egress credit policy"). Treat them as discounts you re-qualify for annually, document the request process in your exit runbook, and re-validate credits actually landed on the invoice — the meter keeps running until the credit posts.
- Unit mismatches corrupt every comparison. AWS TB=1,024 GB, Azure TB=1,000 GB, Google bills GiB; 100 "TB" differs by 2.4% across providers before any rate difference. Our tables above normalize per provider and say so; any vendor "you save X%" slide that does not is marketing, not math.
Who should skip this entire guide
- The 90 percent. If your account-global egress is under ~100 GB/month — which, per AWS's own number, is most of you — the free allowance covers you and every pattern here costs more engineering time than it recovers. Revisit when a media, AI-inference, or data-product workload lands.
- Teams already behind a hot CDN. If your hit ratio is above ~90% and the origin egress is already inside the CloudFront 1 TB allowance, the remaining wins are single-digit percentages; spend the effort on hit-ratio alerting (one dashboard) instead of another architecture migration.
- Single-AZ and single-region shops. No cross-AZ meter, no inter-region meter, and if internet egress is under a terabyte, the Direct Connect port fee alone exceeds the savings.
- Regulated inspection paths. If your compliance regime requires all egress to traverse an inspected TLS-termination point (FINRA-style, some public-sector profiles), the endpoint and CDN patterns may be architecturally incompatible; the fee you are paying is partly a compliance artifact, and the Data Act does not waive compliance either.
- Non-EU teams planning around January 2027. Article 29 protects EU customers (plus existing-contract applicability since September 2025); the AWS move-out waiver is worldwide, but in-parallel multicloud zero-rating is an EU/UK construct. If your billing address is elsewhere, the Data Act lines in the table above are not your levers — the topology patterns are.
The decision table
| Your meter line | The pattern | Verified saving at reference scale |
|---|---|---|
| S3/DynamoDB traffic via NAT gateway | Gateway endpoint (free, route-table) | $675/mo at 500 GB/day; $2,764.80/mo at 2 TB/day |
| Cross-AZ chatter between paired services | Flow-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/month | CloudFront/Front Door with OAC + Origin Shield | −69% at 100 TB with 70% hit; only −9% at 0% hit |
| Egress-dominated object storage, read-heavy | R2/B2/Wasabi zero-egress origin | ≈−93% (storage+ops+egress) for 50 TB store, 100 TB/mo reads |
| Global readers of a single-writer dataset | CRR replicate-once, read local | $204.80 once vs $614.40/mo remote serving (10 TB, 30 TB/mo) |
| Steady hybrid egress to your facility | Direct Connect / ExpressRoute / Interconnect | $2,288.90/mo at 100 TB incl. port, vs $7,980.20 internet |
| Full or service-level cloud exit | Data Act waiver lanes (support ticket) | 100% of switch egress, from 12 Jan 2027 by right, earlier by request |
| EU/UK steady multi-cloud traffic | Google Data Transfer Essentials / Azure at-cost | Qualifying 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.