Estimating Cloud Cost per LiDAR Tile
TL;DR: Run the pipeline on a sample of tiles spanning the density range and record wall time, CPU time, peak memory and bytes read and written for each. Fit cost per tile as compute (instance price × instance-seconds ÷ tiles per instance) plus requests plus storage, then multiply by the tile count with a margin for retries. Compute usually dominates, and it scales with points, not with tile area.
# Context and Motivation
This guide is part of AWS Batch Processing. “How much will it cost to process the county?” is a question to answer before submitting 8,000 array children, not after the bill arrives. The answer is not hard to estimate, but it needs measurement rather than guesses: PDAL runtime varies by an order of magnitude between a sparse rural tile and a dense urban tile covered by several overlapping flightlines, and ground filters such as SMRF scale worse than linearly with point count.
A per-tile model built from twenty or thirty measured tiles predicts the full run well enough to choose between instance types, between spot and on-demand, and between processing everything and processing only what changed.
# Prerequisites and Assumptions
- A working per-tile pipeline and container, as in array jobs for LiDAR tiles in AWS Batch.
- A tile index with point counts per tile (from
pdal info --summaryor the tile index), so the sample can be stratified. - Current prices for your region from the AWS pricing pages. Prices below are placeholders for the arithmetic, not quotes.
# Step-by-Step Implementation
# Step 1 — Sample tiles across the density range
Sort tiles by point count and pick evenly spaced tiles, plus the densest few.
# Step 2 — Measure each run
Record wall time, CPU seconds, peak RSS, and bytes in and out. /usr/bin/time -v or Python’s resource.getrusage gives CPU time and peak memory.
# Step 3 — Fit time against points
A linear fit of seconds against millions of points is usually adequate; check the residuals on the densest tiles.
# Step 4 — Convert to instance cost
From peak memory and vCPUs per child, work out how many children fit per instance, then cost per tile = instance price per second × seconds ÷ children per instance.
# Step 5 — Add storage and requests, then project
Add S3 request and output storage costs per tile, sum over the tile index using each tile’s point count, and add a retry and idle margin.
# Complete Working Example
"""Measure sample tiles, fit a time model, and project the cost of a full run."""
import json
import resource
import subprocess
import time
import numpy as np
SAMPLE = ["tiles/571_4190.laz", "tiles/580_4201.laz", "tiles/598_4222.laz"] # stratified
PIPE = "dtm.json" # the production pipeline, reading from FILENAME
def measure(tile):
t0 = time.time()
r0 = resource.getrusage(resource.RUSAGE_CHILDREN)
subprocess.run(["pdal", "pipeline", PIPE, f"--readers.las.filename=/vsis3/lidar-in/{tile}",
"--writers.gdal.filename=/tmp/out.tif"], check=True)
r1 = resource.getrusage(resource.RUSAGE_CHILDREN)
info = json.loads(subprocess.run(["pdal", "info", "--summary", f"/vsis3/lidar-in/{tile}"],
capture_output=True, text=True, check=True).stdout)
return {"tile": tile, "mpts": info["summary"]["num_points"] / 1e6,
"wall_s": time.time() - t0,
"cpu_s": (r1.ru_utime + r1.ru_stime) - (r0.ru_utime + r0.ru_stime),
"peak_gb": r1.ru_maxrss / 1024**2} # ru_maxrss is KiB on Linux
rows = [measure(t) for t in SAMPLE]
mpts = np.array([r["mpts"] for r in rows])
wall = np.array([r["wall_s"] for r in rows])
slope, intercept = np.polyfit(mpts, wall, 1)
peak = max(r["peak_gb"] for r in rows)
print(f"seconds ≈ {slope:.1f} × Mpts + {intercept:.1f}; peak {peak:.1f} GB")
# ---- projection (placeholder prices: replace with your region's current rates) ----
INSTANCE_PER_HOUR = 0.34 # e.g. an 8 vCPU / 32 GB instance, spot or on-demand
VCPU, MEM_GB = 8, 32
children_per_instance = min(VCPU // 1, int(MEM_GB // (peak * 1.25)))
PUT_PER_1000, GET_PER_1000 = 0.005, 0.0004
STORAGE_GB_MONTH = 0.023
index = json.load(open("tile_index_counts.json")) # {"tile": point_count, ...}
total_s = sum(slope * (n / 1e6) + intercept for n in index.values())
compute = total_s / 3600 / children_per_instance * INSTANCE_PER_HOUR
requests = len(index) * (20 / 1000 * GET_PER_1000 + 2 / 1000 * PUT_PER_1000)
storage = len(index) * 0.05 * STORAGE_GB_MONTH # ~50 MB output per tile
total = (compute + requests + storage) * 1.15 # retries, idle, stragglers
print(f"{len(index)} tiles: compute ${compute:,.0f}, requests ${requests:,.2f}, "
f"storage ${storage:,.2f}/month -> estimate ${total:,.0f}")ru_maxrss from RUSAGE_CHILDREN reports the largest child so far, which is what matters for sizing when you measure one tile per call. On macOS it is in bytes rather than KiB.
# Where Estimates Go Wrong
Three things commonly push real costs above the model. First, stragglers: compute environments scale in instances, and the last hour of a run is often a few long tiles keeping otherwise idle instances alive; sorting the manifest so dense tiles start first, and allowing instances to scale down, trims this. Second, retries: spot reclamation repeats work, and a 5–10% margin is realistic on busy instance families. Third, hidden I/O: reading LAZ over /vsis3/ with a small block size multiplies GET requests, and writing temporary files to EBS adds volume costs. Configure GDAL’s S3 options as in configuring GDAL /vsis3/ for fast point cloud reads.
Things that make costs lower than feared are also worth checking: in-region transfer between S3 and EC2 is free, so moving terabytes of LAZ to compute in the same region does not add transfer charges; and when only a few tiles changed, processing only those — possible when outputs are idempotent and keyed by tile — reduces the run to a fraction of the full cost.
# Key Parameter Table
| Input | Source | Notes |
|---|---|---|
| Seconds per Mpts | Sample fit | Pipeline- and instance-specific |
| Peak memory per tile | ru_maxrss |
Sets children per instance |
| Instance price | AWS pricing | Spot often well below on-demand |
| Children per instance | min(vCPU, memory ÷ peak) | With ~25% memory headroom |
| Requests per tile | S3 access logs or estimate | Depends on block size |
| Output GB per tile | Measured | Storage per month |
| Margin | 10–20% | Retries, stragglers, idle |
# Verification
- Holdout tiles. Predict the runtime of five tiles not in the fit and compare with measured runtimes; a consistent bias means the model needs another term.
- Small real run. Run 1% of the project and compare actual cost from Cost Explorer or tagged billing with the projection for that subset.
- Tag resources. Tag the compute environment and bucket by project so actual spend can be read back per run.
# Gotchas and Edge Cases
Area is not a proxy. Two 1 km tiles can differ tenfold in points. Always model on point counts.
Instance choice changes the slope. A newer CPU generation can cut seconds per million points noticeably; re-measure rather than scaling old results.
Fargate prices differently. Fargate charges per vCPU and GB of memory per second requested, so the memory headroom you declare is billed whether used or not.
# Frequently Asked Questions
What drives the cost of processing a LiDAR tile in the cloud?
Compute time, which grows with the number of points in the tile and the filters applied. S3 requests and storage are usually small in comparison, and transfer within one region is free.
How many tiles should I measure to build a cost model?
Twenty to thirty tiles chosen across the full range of point counts, including the densest few, is usually enough to fit runtime against points and check the fit on holdout tiles.
Why do real runs cost more than the estimate?
Retries after spot interruptions, straggling dense tiles that keep instances alive at the end, and extra S3 requests from small read block sizes. A 10 to 20 percent margin covers most of this.
Does tile area predict processing cost?
Poorly. Point density varies greatly between tiles because of land cover and flightline overlap, so model cost on point counts from the tile index rather than on area.
# Related
- AWS Batch Processing — running PDAL in AWS Batch
- Array Jobs for LiDAR Tiles in AWS Batch — the run being priced
- Handling Spot Interruptions in PDAL Batch Jobs — retries and spot
- Measuring Peak Memory of a PDAL Pipeline — sizing inputs
- Load Balancing Uneven LiDAR Tiles — reducing stragglers