Object Storage Egress Cost Modelling for Geospatial Data

For geospatial workloads, the dominant, controllable egress lever is keeping compute in the same region as storage and putting a CDN in front of anything served to the public. In-region reads to a co-located query engine incur no per-GB transfer fee, so the moment your analytics cluster and your GeoParquet bucket share a region, the egress meter for internal analytics drops to near zero — you pay only request charges. Public-facing tile serving is different: those bytes must leave the cloud, but a CDN absorbs 90–98% of them at the edge, leaving only cache misses to hit origin. Everything else is arithmetic on transferred bytes and request counts, and this page gives you a model for both. It expands the egress meter introduced in the parent guide on cloud cost and storage lifecycle.

Quick-Reference Table

Transfer path Typical rate Dominant meter Primary use case / rationale
In-region to co-located compute 0 $/GB (requests only) Requests Analytics pulls; keep DuckDB/Athena in the storage region
Cross-region replication or read 0.02 $/GB Egress Multi-region serving; replicate once, read locally
Origin to public internet 0.05–0.09 $/GB Egress + requests Direct tile serving without a CDN; avoid at volume
Via CDN edge (cache hit) 0.01–0.05 $/GB CDN delivery Public basemaps and tiles; offloads origin egress

How Egress Accrues for Spatial Workloads

Two distinct spatial access patterns drive egress, and they fail in opposite ways. Analytics pulls move large, contiguous byte ranges — a query engine scanning row groups of a partitioned dataset. Here the byte meter dominates, and the fix is structural: co-locate compute so the transfer never leaves the region, and reduce the bytes read in the first place with compression and column pruning. A dataset stored as compressed GeoParquet columnar storage lets the engine pull only the columns and partitions a query needs, so a 2 TB layer might transfer 30 GB per query rather than 2 TB. The GeoParquet vs FlatGeobuf comparison quantifies how each format’s read pattern affects transferred volume.

Tile serving is the mirror image: enormous request counts moving tiny payloads. A web map view fires dozens of tile GETs, and a popular basemap serves millions of views a day. Individually each tile is 10–40 KB, but the request meter — charged per thousand GETs — accumulates independently of byte volume, and the public-internet egress rate applies to every byte that reaches a browser. The structural fix is a CDN: cache the hot tiles at the edge so the vast majority of requests never reach your origin bucket. Model the offload against your actual cache hit ratio, because the steady trickle of rarely-viewed tiles and high zoom levels still misses to origin.

Compression cuts egress on both paths, but the ratio you can bank depends on the payload. Vector tiles and GeoParquet respond strongly to ZSTD; already-compressed raster tiles (PNG, JPEG) do not compress further at the transport layer. Choose the ratio deliberately using ZSTD compression levels for geospatial data rather than assuming a flat multiplier across payload types.

Python Egress Cost Estimator

The estimator below models both access patterns in one pass. It separates in-region, cross-region, and internet transfer, applies a CDN cache hit ratio to the public path, and reports the request meter alongside the byte meter so you can see which dominates.

python
# Requires: python>=3.10 (standard library only)
from __future__ import annotations

from dataclasses import dataclass


@dataclass(frozen=True)
class EgressRates:
    """Approximate S3-family rates; override per provider and region."""
    internet_gb: float = 0.07      # $/GB origin -> public internet
    cross_region_gb: float = 0.02  # $/GB to another region
    cdn_gb: float = 0.02           # $/GB served from CDN edge
    request_per_k: float = 0.0004  # $ per 1,000 GET


@dataclass
class Workload:
    name: str
    monthly_requests: int          # total GETs from clients
    avg_object_kb: float           # payload per request after compression
    internet_fraction: float       # share served to public internet
    cross_region_fraction: float   # share crossing regions
    cdn_hit_ratio: float = 0.0     # share of internet requests cached at edge


def estimate_egress(w: Workload, rates: EgressRates | None = None) -> dict[str, float]:
    """Return monthly egress + request cost for one spatial workload."""
    rates = rates or EgressRates()
    if not 0.0 <= w.cdn_hit_ratio <= 1.0:
        raise ValueError(f"cdn_hit_ratio out of range for {w.name}")

    total_gb = w.monthly_requests * w.avg_object_kb / 1_048_576  # KB -> GB

    # Split byte volume by transfer path
    internet_gb = total_gb * w.internet_fraction
    cross_gb = total_gb * w.cross_region_fraction
    # In-region remainder transfers at zero per-GB cost

    # CDN offload applies only to the public-internet share
    origin_internet_gb = internet_gb * (1.0 - w.cdn_hit_ratio)
    cdn_gb = internet_gb * w.cdn_hit_ratio

    byte_cost = (
        origin_internet_gb * rates.internet_gb
        + cdn_gb * rates.cdn_gb
        + cross_gb * rates.cross_region_gb
    )

    # Requests are charged whether or not bytes leave the region.
    # Cache hits are absorbed by the CDN, not the origin bucket.
    origin_requests = w.monthly_requests * (
        1.0 - w.internet_fraction * w.cdn_hit_ratio
    )
    request_cost = origin_requests / 1000 * rates.request_per_k

    return {
        "transfer_gb": round(total_gb, 2),
        "byte_cost": round(byte_cost, 2),
        "request_cost": round(request_cost, 2),
        "total": round(byte_cost + request_cost, 2),
    }


if __name__ == "__main__":
    workloads = [
        Workload("public-basemap-tiles", monthly_requests=250_000_000,
                 avg_object_kb=18, internet_fraction=1.0,
                 cross_region_fraction=0.0, cdn_hit_ratio=0.96),
        Workload("analytics-scan", monthly_requests=40_000,
                 avg_object_kb=32_000, internet_fraction=0.0,
                 cross_region_fraction=0.0),  # in-region, egress-free bytes
        Workload("multi-region-serving", monthly_requests=2_000_000,
                 avg_object_kb=40, internet_fraction=0.0,
                 cross_region_fraction=1.0),
    ]
    for w in workloads:
        print(f"{w.name:24s} {estimate_egress(w)}")

The output makes the two patterns concrete. The in-region analytics scan transfers over a terabyte but costs cents, because same-region bytes are free and only requests are metered. The public tile workload transfers less but costs more, and its bill is set almost entirely by the 4% of requests that miss the CDN.

Validation

Reconcile the model against the DataTransfer and Requests usage types in your cost-and-usage report, filtered to the bucket and prefix that back each workload. The CLI check below pulls the last month’s transfer cost per usage type so you can compare it against the estimator.

bash
aws ce get-cost-and-usage \
  --time-period Start=2026-06-01,End=2026-07-01 \
  --granularity MONTHLY \
  --metrics UnblendedCost \
  --filter '{"Dimensions":{"Key":"USAGE_TYPE_GROUP","Values":["S3: Data Transfer","S3: Requests"]}}' \
  --group-by Type=DIMENSION,Key=USAGE_TYPE

Expected ranges for a healthy setup: in-region analytics should show a DataTransfer figure near zero with all cost in Requests; a CDN-fronted public workload should show origin DataTransfer an order of magnitude below the CDN’s own reported delivery. If in-region analytics shows non-trivial DataTransfer, a consumer is reading across regions — the most common and most expensive misconfiguration.

Edge Cases and Caveats

NAT gateway data-processing charges. When a private-subnet compute node reads from object storage through a NAT gateway instead of a VPC gateway endpoint, you pay a per-GB data-processing fee even though the traffic never leaves the region. For terabyte-scale scans this quietly rivals internet egress. Route S3 traffic through a gateway endpoint to eliminate it.

Cross-cloud analytics. Reading a bucket in one provider from a compute engine in another treats every byte as internet egress at the highest rate. If a partner insists on a different cloud, replicate the working set once into their provider and read locally rather than paying egress on every query.

CDN hit ratio collapse on high-zoom tiles. Cache hit ratios reported as a global average hide the fact that deep-zoom and rarely-viewed tiles miss almost every time. If your traffic skews toward high zoom levels or unusual extents, the effective origin egress can be several times the headline model. Segment the hit ratio by zoom band before trusting a single number, and consider pre-warming the cache for known-popular areas.

Frequently Asked Questions

Is object storage egress free if my compute is in the same region?

Usually yes for same-region, same-provider access — reads from S3 to EC2 in the same region incur no per-GB transfer fee, only request charges. You still pay egress when data crosses to another region, leaves for the public internet, or moves to a different cloud. This is why co-locating your query engine in the storage region is the single largest egress lever for analytics pulls.

How much does a CDN reduce tile-serving egress cost?

A CDN reduces origin egress by its cache hit ratio, which for popular basemap tiles routinely reaches 90–98%. At a 95% hit ratio, only 5% of tile bytes are served from origin, and CDN delivery rates are often lower than raw object-storage egress. The catch is that cold or rarely-requested tiles still miss to origin, so model the hit ratio against your real zoom and extent distribution rather than assuming a flat number.

Do per-request charges matter more than byte egress for tile serving?

Often yes. A 256x256 tile may be only 10–40 KB, so a million tile views transfer 10–40 GB but generate a million GET requests. At 0.0004 USD per thousand GETs that is 0.40 USD in requests against a similar or smaller egress figure — and both scale with traffic. Serving vector tiles from fewer, larger partitioned objects or batching reads lowers the request meter directly.


← Back to Cloud Cost & Storage Lifecycle for Spatial Data