Storage Classes and the Egress Bill
Object, block and file storage, when a cheaper storage class costs more, retrieval fees and minimum durations, and the CDN arithmetic that pays for itself in a week.
Storage is cheap. Moving data is not, and the exam-prep framing of “which class for which access pattern” hides the arithmetic that actually decides.
Three kinds of storage
OBJECT BLOCK FILE
example S3, GCS, Blob EBS, PD EFS, Filestore
interface HTTP API device, needs FS NFS / SMB
access whole object random byte random byte
attached to nothing one instance* many instances
capacity effectively ∞ provisioned, fixed elastic
price /GB-month ~$0.023 ~$0.08 ~$0.30
latency 10-100 ms <1 ms 1-10 ms
durability 11 nines replicated in-AZ multi-AZ
best for media, backups, databases, boot shared config,
logs, data lakes volumes legacy apps
* Multi-attach exists for some block volume types and is a specialist feature, not a default.
The price column is the story: file storage is roughly 13x object storage per gigabyte. An answer that puts a data lake on a shared filesystem is expensive in a way that is easy to miss.
“The question I’d ask first is whether the access pattern is whole-object or random-byte. If the application reads an entire file, object storage is right and an order of magnitude cheaper. If it needs a filesystem — a database data directory, an app that mmaps — then block, and I’d only reach for a shared filesystem if several instances genuinely need the same mutable directory.”
Storage classes: the fee that is not in the headline price
# classes.py
CLASSES = {
# $/GB-mo retrieval $/GB min days first-byte
"standard": (0.0230, 0.0000, 0, "ms"),
"infrequent access": (0.0125, 0.0100, 30, "ms"),
"one-zone IA": (0.0100, 0.0100, 30, "ms"),
"glacier instant": (0.0040, 0.0300, 90, "ms"),
"glacier flexible": (0.0036, 0.0100, 90, "1-5 min"),
"deep archive": (0.0009, 0.0200, 180, "12 hours"),
}
SIZE_GB = 100_000 # 100 TB
MONTHS = 12
print(f"{'class':<18} {'store/yr':>11} {'+1 full read':>13} {'+4 reads':>11} {'+12 reads':>11}")
for name, (store, retrieve, mindays, _) in CLASSES.items():
base = SIZE_GB * store * MONTHS
r1 = base + SIZE_GB * retrieve
r4 = base + SIZE_GB * retrieve * 4
r12 = base + SIZE_GB * retrieve * 12
print(f"{name:<18} {base:>11,.0f} {r1:>13,.0f} {r4:>11,.0f} {r12:>11,.0f}")
$ python classes.py
class store/yr +1 full read +4 reads +12 reads
standard 27,600 27,600 27,600 27,600
infrequent access 15,000 16,000 19,000 27,000
one-zone IA 12,000 13,000 16,000 24,000
glacier instant 4,800 7,800 16,800 40,800
glacier flexible 4,320 5,320 8,320 16,320
deep archive 1,080 3,080 9,080 25,080
Read the last column. Glacier Instant at monthly access costs $40,800 — 48% more than standard, despite a headline price 83% lower. Deep archive read monthly costs nearly as much as standard.
The pattern generalises: cheaper storage means a higher retrieval fee, and past a certain access frequency the retrieval fee dominates completely.
Two more traps not in the price:
- Minimum storage duration. Deleting an infrequent-access object after 3 days still bills 30 days. A lifecycle policy that transitions objects at 7 days and expires them at 14 costs more than leaving them in standard.
- Per-object minimum size. Infrequent-access tiers bill a minimum of 128 KB per object. Ten million 4 KB objects are billed as 128 KB each — a 32x overcharge that lifecycle policies cause routinely.
objects, real_kb, billed_kb = 10_000_000, 4, 128
print(f"real: {objects*real_kb/1e6:>8,.1f} GB ${objects*real_kb/1e6*0.0125:>8,.2f}/mo")
print(f"billed: {objects*billed_kb/1e6:>8,.1f} GB ${objects*billed_kb/1e6*0.0125:>8,.2f}/mo")
real: 40.0 GB $ 0.50/mo
billed: 1,280.0 GB $ 16.00/mo
Small numbers here, but the ratio is the point, and it scales.
“Lifecycle policies are not free. Before transitioning to a cheaper class I’d check three things: how often the data is actually read, the object size distribution against the 128 KB minimum, and whether the objects live longer than the minimum duration. I’ve seen a lifecycle policy increase a bill.”
The egress arithmetic
# egress.py
TB = 1024
TIERS = [(10*TB, 0.09), (40*TB, 0.085), (100*TB, 0.07), (float('inf'), 0.05)]
def internet_egress(gb):
cost, remaining, prev = 0.0, gb, 0
for limit, rate in TIERS:
band = min(remaining, limit - prev)
cost += band * rate
remaining -= band
prev = limit
if remaining <= 0: break
return cost
for tb in (1, 10, 50, 200, 1000):
gb = tb * TB
print(f"{tb:>5} TB out internet ${internet_egress(gb):>12,.2f}"
f" cross-region ${gb*0.02:>10,.2f} cross-AZ ${gb*0.01:>10,.2f}")
$ python egress.py
1 TB out internet $ 92.16 cross-region $ 20.48 cross-AZ $ 10.24
10 TB out internet $ 921.60 cross-region $ 204.80 cross-AZ $ 102.40
50 TB out internet $ 4,249.60 cross-region $ 1,024.00 cross-AZ $ 512.00
200 TB out internet $ 12,953.60 cross-region $ 4,096.00 cross-AZ $ 2,048.00
1000 TB out internet $ 53,913.60 cross-region $ 20,480.00 cross-AZ $ 10,240.00
A petabyte out to the internet is $53,900 a month. The same petabyte stored costs 1,024,000 GB × $0.023 ≈ $23,550 — serving it out once a month costs more than twice what storing it costs, and the tiering only softens that at very high volume.
The direction matters and is a common interview probe:
DIRECTION TYPICAL CHARGE
internet → cloud (ingress) free
cloud → internet (egress) $0.05-0.09/GB, tiered
same AZ, private IP free
cross-AZ, same region $0.01/GB each direction
cross-region $0.02/GB
via NAT gateway $0.045/GB on top of everything else
via VPC endpoint (gateway type) free
CDN → internet $0.02-0.085/GB, cheaper than origin
Ingress is free and egress is not. That asymmetry is deliberate and is the mechanism behind cloud lock-in — getting data in costs nothing, getting it out costs real money.
The NAT gateway line
aws ec2 describe-nat-gateways --query 'NatGateways[].{id:NatGatewayId,state:State}' --output table
--------------------------------------------
| DescribeNatGateways |
+----------------------+-------------------+
| id | state |
+----------------------+-------------------+
| nat-0a1b2c3d4e5f6g7 | available |
+----------------------+-------------------+
HOURS = 730
hourly = 0.045 * HOURS
for tb in (1, 10, 50, 200):
gb = tb * 1024
print(f"{tb:>4} TB through NAT hourly ${hourly:>7,.2f} + data ${gb*0.045:>10,.2f}"
f" = ${hourly + gb*0.045:>10,.2f}")
1 TB through NAT hourly $ 32.85 + data $ 46.08 = $ 78.93
10 TB through NAT hourly $ 32.85 + data $ 460.80 = $ 493.65
50 TB through NAT hourly $ 32.85 + data $ 2,304.00 = $ 2,336.85
200 TB through NAT hourly $ 32.85 + data $ 9,216.00 = $ 9,248.85
The hourly rate is trivial. The per-gigabyte rate is not, and the traffic people forget is private instances reading from object storage — which routes through the NAT by default:
aws ec2 create-vpc-endpoint --vpc-id vpc-0abc --service-name com.amazonaws.us-east-1.s3 \
--route-table-ids rtb-0def --vpc-endpoint-type Gateway
{
"VpcEndpoint": {
"VpcEndpointId": "vpce-0123456789abcdef0",
"VpcEndpointType": "Gateway",
"State": "available"
}
}
A gateway endpoint costs nothing and removes that traffic from the NAT entirely. It is one API call and, on a data-heavy workload, the single largest saving available.
“The first thing I’d check on a surprising bill is whether object storage traffic is going through a NAT gateway. A gateway VPC endpoint is free, takes one API call, and I’ve seen it cut a five-figure monthly line to zero.”
The CDN calculation
# cdn.py
TB = 1024
traffic_gb = 100 * TB
ORIGIN_EGRESS, CDN_EGRESS = 0.085, 0.020
no_cdn = traffic_gb * ORIGIN_EGRESS
print(f"no CDN, all from origin: ${no_cdn:>12,.2f}\n")
print(f"{'hit rate':>9} {'origin':>12} {'CDN':>12} {'total':>12} {'saved':>12} {'%':>6}")
for hit in (0.50, 0.70, 0.85, 0.95, 0.99):
origin = traffic_gb * (1 - hit) * ORIGIN_EGRESS
cdn = traffic_gb * CDN_EGRESS
total = origin + cdn
print(f"{hit:>8.0%} {origin:>12,.2f} {cdn:>12,.2f} {total:>12,.2f}"
f" {no_cdn-total:>12,.2f} {100*(no_cdn-total)/no_cdn:>5.1f}%")
$ python cdn.py
no CDN, all from origin: $ 8,704.00
hit rate origin CDN total saved %
50% 4,352.00 2,048.00 6,400.00 2,304.00 26.5%
70% 2,611.20 2,048.00 4,659.20 4,044.80 46.5%
85% 1,305.60 2,048.00 3,353.60 5,350.40 61.5%
95% 435.20 2,048.00 2,483.20 6,220.80 71.5%
99% 87.04 2,048.00 2,135.04 6,568.96 75.5%
At a realistic 85% hit rate the transfer bill drops 61%, and latency improves for every user near an edge location. Those are two separate wins and both are worth naming.
The hit rate is the variable, and it is a function of cache headers and content mix:
CONTENT REALISTIC HIT RATE
static assets, versioned 99%+ — immutable, cache forever
images, video 90-98%
API responses, cacheable 50-80% — depends on the TTL you can accept
personalised HTML near 0% — do not put it behind a CDN for caching
Recognising it
SIGNAL ANSWER
"we store a lot and read rarely" archive tier — check minimum duration first
"we store a lot and read constantly" standard; a cheaper class will cost more
"lots of tiny objects" watch the 128 KB per-object minimum
"the bill jumped and nothing changed" check NAT gateway and cross-AZ transfer
"serving media / static content" CDN, and quote the hit-rate arithmetic
"private instances reading object storage" gateway VPC endpoint, free
"multi-region for latency" cross-region replication — price the transfer
"database data directory" block storage, not object
"several instances need one mutable dir" shared filesystem, and question the design
"how do we get 500 TB out of this cloud" egress is the answer nobody likes: ~$35,000
Practice
1. Price 100 TB in each storage class with 12 reads a year.
standard $27,600 glacier instant $40,800 deep archive $25,080
The headline price is 83% lower and the annual cost is 48% higher. Retrieval fees dominate past a few reads a year.
2. Bill 10 million 4 KB objects in an infrequent-access tier.
real 40 GB ($0.50/mo) billed 1,280 GB ($16.00/mo) 32x
The 128 KB per-object minimum. Lifecycle policies cause this routinely and it does not appear in the storage class comparison table.
3. Price 200 TB flowing through a NAT gateway.
hourly $32.85 + data $9,216.00 = $9,248.85/month
A gateway VPC endpoint removes object-storage traffic from that line entirely, costs nothing, and takes one API call.
4. Compute the CDN saving at an 85% hit rate.
$8,704 → $3,354, a 61% reduction — plus latency improvement
Two separate wins. Name both, and name the content types where the hit rate will be near zero.
Next: networking and VPCs — the debug ladder for “why can’t these two things talk to each other”.