Comparing the Cost of Lambda, Cloud Run and Azure Functions for Tiling
Priced per vCPU-second, the three platforms are within 5% of each other — $0.0000288 on AWS Lambda, $0.0000290 on GCP Cloud Run and about $0.0000276 on Azure Functions Consumption, at the time of writing. Priced per real tiling job, they are not: the same 6,400-window scene costs $1.69 on Lambda, $1.77 on Azure Functions and $0.56 on Cloud Run — and the Cloud Run figure becomes $2.09 if you set --concurrency 1. The rate card is a red herring; per-instance concurrency is the whole comparison.
Context
A raster tile is a bad fit for the per-invocation billing model that all three platforms grew up with. Roughly 57% of a warm 512-pixel tile’s wall clock is object-storage wait, as broken down in cost-per-tile math for Lambda raster jobs. On a runtime that serves one request per execution environment, you are billed for that wait at the full memory allocation. On a runtime that serves eight, you are billed for it once.
That is the only structural difference between the three that survives a price change, so this guide normalises the rate cards first, then prices one identical job on each, then gives you a harness that reproduces the measurement against your own scenes. The wider cost picture — orchestration, network egress and log ingestion, all of which are provider-specific and all of which are excluded here so the runtimes compare like for like — is in Cost Modelling for Serverless Raster Pipelines.
Prerequisites
- One container image that runs everywhere. The comparison is meaningless if the Lambda build uses a layer and the Cloud Run build uses a different GDAL version. Build once from a multi-stage Dockerfile for GDAL on Cloud Run and deploy the same digest to all three.
- Pinned GDAL and PROJ versions. A decode-speed difference between GDAL 3.6 and 3.9 is larger than the rate difference you are trying to measure — see pinning GDAL and PROJ versions across build and runtime.
- Source data co-located with each runtime. Reading S3 from Cloud Run measures the internet, not the platform. Stage a copy in S3, GCS and Azure Blob Storage in matched regions.
- The same window manifest on all three. Identical offsets, identical band subset, identical output codec.
- Current published rates. Everything here is us-east-1, us-central1 and East US list pricing at the time of writing.
Normalising the rate cards
The three providers bill different units, so they cannot be compared until you convert to a common one. A vCPU-second is the natural choice for a compute-bound comparison, and getting there requires knowing how much RAM each platform forces you to buy alongside it.
On Lambda, memory and vCPU are one dial at 1,769 MB per vCPU, so a vCPU-second costs 0.0000166667 × (1769 / 1024) = $0.0000288. On Cloud Run the dials are independent, so the answer depends on the shape you pick; at a typical 2 GiB per vCPU it is 0.000024 + 2 × 0.0000025 = $0.0000290. On Azure Functions Consumption you cannot choose at all — the plan gives roughly one vCPU with the full 1,536 MB, which works out near $0.0000276 per vCPU-second.
Within 5%. Any argument that starts “platform X is cheaper per GB-second” is comparing units that do not mean the same thing.
The same job on all three
--concurrency.The job is one 40,960 × 40,960 pixel four-band uint16 scene, tiled at 512 pixels into 6,400 windows, each read from same-region object storage, NDVI-computed and written back deflate-compressed. Compute and request charges only.
| Configuration | Per-tile behaviour | Total | Indexed |
|---|---|---|---|
Cloud Run — 4 vCPU / 8 GiB, --concurrency 8 |
6.0 s wall, 8 tiles in flight per instance | $0.56 | 0.33× |
| AWS Lambda — 3,008 MB (1.70 vCPU) | 5.4 s, one tile per environment | $1.69 | 1.00× |
| Azure Functions — 1,536 MB Consumption | 11.5 s, one tile per worker | $1.77 | 1.05× |
Cloud Run — 1 vCPU / 4 GiB, --concurrency 1 |
9.6 s wall, one tile per instance | $2.09 | 1.24× |
| AWS Lambda — 10,240 MB (5.79 vCPU) | 2.4 s, one tile per environment | $2.56 | 1.51× |
Read the two Cloud Run rows together. They use the same rate card, the same image and the same scene, and they differ by 3.7×. Everything between them is --concurrency, plus the instance shape chosen to feed it. With eight tiles in flight, one instance-second of 4 vCPU covers eight tiles’ worth of range-read latency, so the scene needs about 19,200 vCPU-seconds instead of the 58,700 vCPU-seconds Lambda bills for the same work. With concurrency 1, the container loses that advantage and picks up two costs Lambda does not have: instance startup and the scale-down tail after the last request.
Azure Functions Consumption illustrates the opposite trap. Its published GB-second rate is the lowest of the three and its bill is the second-highest, because the 1,536 MB ceiling sits below the memory tier where a four-band GDAL job stops thrashing its block cache, and there is no larger Consumption tier to escape to. It pays the lowest rate for twice as many seconds. The 10 min / 1,536 MB envelope is a hard boundary rather than a tuning target, as set out in Timeout Ceiling Comparison for Long-Running Geospatial Jobs and Ephemeral Storage Comparison Across Serverless Platforms.
The Lambda 10,240 MB row is worth keeping in view too: it is the most expensive configuration in the table and also the fastest per tile, at 2.4 seconds against 5.4. If a backfill has a deadline, 1.51× is a defensible price for 2.25× the throughput — the sizing rationale is in how to configure 10 GB memory for AWS Lambda raster processing.
Implementation: a harness that measures your scenes
Do not port the numbers above. Port the measurement. The script below takes the platform-native usage figure each provider reports — billed duration on AWS, billable instance time on GCP, execution units on Azure — and reduces all three to one cost per scene.
# platform_cost_compare.py — one cost per scene from each platform's own metric.
from dataclasses import dataclass
# us-east-1 / us-central1 / East US list prices at the time of writing.
AWS_GB_SECOND = 0.0000166667
AWS_REQUEST = 0.20 / 1e6
GCP_VCPU_SECOND = 0.000024
GCP_GIB_SECOND = 0.0000025
GCP_REQUEST = 0.40 / 1e6
AZ_GB_SECOND = 0.000016
AZ_EXECUTION = 0.20 / 1e6
@dataclass
class LambdaRun:
"""From CloudWatch: Sum(Duration) in ms, Sum(Invocations)."""
memory_mb: int
total_duration_ms: float
invocations: int
def usd(self) -> float:
gb_s = (self.memory_mb / 1024) * (self.total_duration_ms / 1000)
return gb_s * AWS_GB_SECOND + self.invocations * AWS_REQUEST
@dataclass
class CloudRunRun:
"""From Cloud Monitoring: billable_instance_time in seconds."""
vcpu: float
memory_gib: float
billable_instance_seconds: float
requests: int
def usd(self) -> float:
return (self.billable_instance_seconds
* (self.vcpu * GCP_VCPU_SECOND + self.memory_gib * GCP_GIB_SECOND)
+ self.requests * GCP_REQUEST)
@dataclass
class AzureFunctionsRun:
"""From Azure Monitor: FunctionExecutionUnits in MB-milliseconds."""
execution_units_mb_ms: float
executions: int
def usd(self) -> float:
gb_s = self.execution_units_mb_ms / 1_024_000
return gb_s * AZ_GB_SECOND + self.executions * AZ_EXECUTION
if __name__ == "__main__":
tiles = 6400
runs = {
"Lambda 3,008 MB": LambdaRun(3008, 5.4 * 1000 * tiles, tiles),
"Lambda 10,240 MB": LambdaRun(10240, 2.4 * 1000 * tiles, tiles),
"Cloud Run 4vCPU/8GiB c=8": CloudRunRun(4, 8, 6.0 * tiles / 8, tiles),
"Cloud Run 1vCPU/4GiB c=1": CloudRunRun(1, 4, 9.6 * tiles, tiles),
"Azure Consumption 1,536 MB": AzureFunctionsRun(1536 * 11_500 * tiles, tiles),
}
baseline = runs["Lambda 3,008 MB"].usd()
for name, run in sorted(runs.items(), key=lambda kv: kv[1].usd()):
cost = run.usd()
print(f"{name:<28} ${cost:>6.3f} {cost / baseline:>5.2f}x")
Verification
Run one scene on each platform and confirm the harness reproduces the provider’s own billing view:
python platform_cost_compare.py
Expected output:
Cloud Run 4vCPU/8GiB c=8 $ 0.559 0.33x
Lambda 3,008 MB $ 1.693 1.00x
Azure Consumption 1,536 MB $ 1.768 1.04x
Cloud Run 1vCPU/4GiB c=1 $ 2.092 1.24x
Lambda 10,240 MB $ 2.562 1.51x
Then reconcile each figure against the provider’s console: AWS Cost Explorer filtered to the Lambda-GB-Second usage type, the GCP billing export filtered to Cloud Run CPU and memory SKUs, and the Azure cost analysis blade filtered to the function app’s resource group. A model within 3% of all three is trustworthy enough to choose a platform on. If Cloud Run reads high, check container/instance_count — an over-generous --min-instances setting bills warm instances between scenes and quietly erases the concurrency advantage, which is the same trade discussed in reducing Cloud Functions 2nd gen cold starts with min instances.
Choosing between them
The panels above summarise the fit. The number that decides it is the fraction of tile wall clock spent waiting on object storage, and it is worth measuring before anything else: above 50%, in-process concurrency beats any rate difference, and below it the three platforms converge to within about 10% of each other and the choice should be made on ceilings, packaging and where the data already lives. The vector-side equivalent of the same conclusion is worked through in the vector tile pipeline with Cloud Run and Pub/Sub, where per-instance concurrency is the reason the pipeline is affordable at all.
Gotchas
-
--concurrencyabove 1 needs a thread-safe handler. GDAL is thread-safe per dataset handle, not across them. Open a freshrasteriodataset inside each request rather than sharing one at module scope, and sizeGDAL_CACHEMAXfor the sum of concurrent tiles, not one — 8 GiB across concurrency 8 means about 250 MB of cache each, not 2 GB. -
Cloud Run throttles CPU between requests unless you say otherwise. Without
--no-cpu-throttling, an instance drops to a fraction of a vCPU whenever no request is being handled, which stalls background GDAL work and inflates wall time. It also changes the billing mode, so check the rate you are actually being charged before comparing. -
Azure
FunctionExecutionUnitsis in MB-milliseconds, not GB-seconds. Divide by 1,024,000. Comparing the raw number against a GB-second figure overstates Azure by roughly a thousand-fold and is the single most common error in these comparisons. -
The free grants make small pipelines look identical. 400,000 GB-seconds on AWS and Azure, 180,000 vCPU-seconds plus 360,000 GiB-seconds on GCP. At 3,008 MB and 5.4 seconds a tile, AWS’s grant covers about four scenes a month. Benchmark above the grant or you are measuring nothing.
Frequently Asked Questions
Which serverless platform is cheapest for raster tiling?
None of them by rate. A vCPU-second costs $0.0000288 on Lambda, $0.0000290 on Cloud Run and about $0.0000276 on Azure Functions Consumption — within 5%. Cloud Run wins a real tiling job by roughly 3×, but only because it serves many requests per instance and therefore stops charging you separately for each tile’s object-storage wait.
Why does Cloud Run concurrency change the bill so much?
Cloud Run bills instance time, not request time. At --concurrency 8, eight tiles share one instance-second, so the ~57% of each tile spent waiting on object storage is paid once rather than eight times. At --concurrency 1 the same container is about 24% more expensive than Lambda, because you also pay for instance startup and the scale-down tail.
Can Azure Functions on the Consumption plan run raster tiling at all?
Only for small windows. The plan caps at 1,536 MB and 10 minutes with roughly one vCPU, below the memory tier where a four-band GDAL job stops thrashing its block cache. Tiles run about twice as slowly as on a 3,008 MB Lambda, so the lowest published GB-second rate of the three still produces a slightly higher bill. Above that envelope, move to Premium or Azure Container Apps.
Do the free tiers change the comparison?
Only at very small scale. AWS and Azure each grant 400,000 GB-seconds a month; GCP grants 180,000 vCPU-seconds plus 360,000 GiB-seconds and 2 million requests. At 3,008 MB and 5.4 seconds per tile, AWS’s grant covers about four scenes. Any backfill exhausts it within the first hour, so never model a production pipeline with the grant included.
Related
- Cost Modelling for Serverless Raster Pipelines — the orchestration, network and log charges this comparison deliberately excludes
- Cost-per-Tile Math for Lambda Raster Jobs — where the 57% object-storage wait figure comes from
- Timeout Ceiling Comparison for Long-Running Geospatial Jobs — the 15 / 60 / 10 minute envelopes that constrain which of these configurations are even available
- Ephemeral Storage Comparison Across Serverless Platforms — scratch-disk semantics on each runtime, which differ more than the pricing does
- Multi-Stage Dockerfile for GDAL on Cloud Run — building the one image this comparison requires