Choosing Instance Types for Raster Workloads
Compare instances by cost per tile, not by price per hour:
def cost_per_tile(price_per_hour: float, tiles: int, seconds: float) -> float:
return price_per_hour * (seconds / 3600) / tiles
print(cost_per_tile(0.68, 400, 1520)) # c7g.4xlarge example
print(cost_per_tile(1.01, 400, 1210)) # m7i.4xlarge example
The cheapest instance per hour is rarely the cheapest per result. Raster work is usually bound by memory or network before CPU, and the right machine is the one whose bottleneck matches the workload’s. This page belongs to optimizing pipeline cost and performance in Cloud Execution & Orchestration.
Three Resources, Three Families
The deciding measurement is peak memory per concurrent task. If a task peaks at 3 GB and each core runs one task, compute-optimised machines with 2 GB per core will spill or be killed, and paying for fewer, bigger tasks per machine wastes the cores you bought. Profiling one representative tile, as in profiling memory in a raster worker, gives this number before any money is spent.
Environment & Setup
| Package | Version pin | Used for |
|---|---|---|
boto3 |
>=1.34 |
Price and instance metadata |
dask[distributed] |
>=2023.1 |
The workload under test |
pandas |
>=2.0 |
Comparing benchmark results |
pip install "boto3>=1.34" "dask[distributed]>=2023.1" "pandas>=2.0"
Complete Working Example
import pandas as pd
# Results from running the same 400-tile benchmark on each candidate
runs = pd.DataFrame([
# type, vcpu, mem_gb, net_gbps, usd_h, seconds, spilled_gb
("c7g.4xlarge", 16, 32, 15.0, 0.58, 1580, 11.2),
("m7g.4xlarge", 16, 64, 15.0, 0.65, 1330, 0.0),
("r7g.4xlarge", 16, 128, 15.0, 0.86, 1320, 0.0),
("m7i.4xlarge", 16, 64, 12.5, 0.81, 1250, 0.0),
("m7g.8xlarge", 32, 128, 15.0, 1.31, 690, 0.0),
], columns=["type", "vcpu", "mem_gb", "net_gbps", "usd_h", "seconds", "spilled_gb"])
TILES = 400
runs["usd_per_tile"] = runs.usd_h * runs.seconds / 3600 / TILES
runs["tiles_per_h"] = TILES / (runs.seconds / 3600)
runs["gb_per_vcpu"] = runs.mem_gb / runs.vcpu
print(runs.sort_values("usd_per_tile")[["type", "gb_per_vcpu", "usd_per_tile", "tiles_per_h", "spilled_gb"]])
The benchmark shows the typical pattern. The compute-optimised machine is cheapest per hour but spills to disk, so it is slower and not cheapest per tile. The memory-optimised machine does not spill but is paying for memory the workload does not use. The general-purpose ARM machine wins on cost per tile, and the larger size of the same family is nearly twice as fast for twice the price — a sign the workload scales cleanly and that the choice between sizes can be made on deadline rather than cost.
Network Is the Hidden Resource
Instance listings quote network bandwidth “up to” some figure for smaller sizes, meaning a burst allowance that runs out after minutes. A pipeline reading COGs for hours runs at the baseline, which on the smallest sizes can be a fraction of a gigabit. On read-heavy workloads, a fleet of small instances with the same total cores as a few large ones is then network-starved. Measuring actual throughput, as in benchmarking COG read throughput from object storage, on the instance size you plan to use avoids the surprise.
ARM, Spot and Regions
ARM-based instances such as AWS Graviton are typically 15–25% cheaper for similar performance, and the geospatial stack — GDAL, rasterio, NumPy, Dask — has mature ARM wheels and conda packages. The main cost is building multi-architecture container images, covered in building a slim GDAL Docker image. Spot capacity cuts compute cost by 60–70% and suits tile pipelines well, since each tile is independent and can be retried; diversify across several instance types of the same shape so a capacity shortage in one does not stall the run. Finally, run compute in the same region as the data — cross-region reads add egress charges that can exceed the instance cost itself.
Local Disk and Scratch Space
Some raster steps need fast local storage: Dask spilling to disk, GDAL writing a temporary GeoTIFF before converting to COG, or warping large mosaics with an on-disk cache. Instance families with a d suffix carry local NVMe drives that are far faster than network-attached volumes, and for spill-heavy or write-heavy jobs they can shorten runtime enough to pay for themselves. For pipelines that stream reads from object storage and write results directly back, local disk is irrelevant and a small root volume is enough. Check where temporary files actually land — TMPDIR, CPL_TMPDIR for GDAL, and Dask’s temporary-directory — and point them at the fast disk when one is present.
Revisit the Choice
Instance generations change every year or two, usually with better price-performance, and a pipeline’s workload drifts as features are added. Re-run the benchmark whenever the pipeline changes substantially or a new generation appears; it takes an hour and often saves a noticeable share of the monthly bill. Keep the benchmark script and its tile list in the repository so the comparison is repeatable and genuinely like for like across generations.
Verification
best = runs.sort_values("usd_per_tile").iloc[0]
assert best.spilled_gb == 0, "cheapest option spills; memory per core is too low"
print(f"choose {best.type}: ${best.usd_per_tile * 1000:.2f} per 1,000 tiles")
Common Errors
The cheapest instance per hour costs the most per run
It spills or is network-starved. Compare cost per tile from a real benchmark.
Performance collapses after the first few minutes
Burst network credits ran out. Size for baseline bandwidth or use larger instances.
Container fails to start on ARM instances
The image was built for x86 only. Build a multi-architecture image.
Spot runs stall for hours
Only one instance type was allowed. Diversify across similar types and sizes.
Frequently Asked Questions
Q: Which instance family suits raster processing? General-purpose families with about 4 GB per vCPU suit most tile pipelines. Use compute-optimised only when memory per task is small, and memory-optimised for long time-series cubes or large reprojections.
Q: Are ARM instances suitable for GDAL work? Yes. GDAL, rasterio, NumPy and Dask all have mature ARM builds, and ARM instances are usually cheaper for similar throughput.
Q: Should I use many small instances or a few large ones? For read-heavy work, fewer larger instances, because baseline network bandwidth scales with size and small instances are throttled.
Q: Is spot capacity safe for raster pipelines? For independent, retryable tile jobs, yes. Checkpoint per tile, diversify instance types, and keep long single tasks on on-demand capacity.
Related
- Optimizing Pipeline Cost and Performance — the parent topic.
- Choosing between Threads and Processes for GDAL Workloads — laying out workers on the chosen machine.
- Sizing AWS Batch Jobs for Tile Workloads — the job side of the same decision.
- Reducing S3 Egress Costs in Raster Pipelines — the other half of the bill.