Skip to content

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.

Effective price of a vCPU-second on Lambda, Cloud Run and Azure FunctionsComparison grid across AWS Lambda, GCP Cloud Run and Azure Functions Consumption showing the billing unit, the published rate, the RAM you are forced to buy per vCPU, the effective cost of one vCPU-second, the request charge, and the monthly free grant. The three effective vCPU-second prices are 0.0000288, 0.0000290 and 0.0000276 dollars, within five percent of each other.Normalise the rate cards and they nearly vanishAWS LambdaGCP Cloud RunAzure Functions(Consumption)Billing unitGB-secondvCPU-s + GiB-sGB-secondPublished rate$0.0000166667per GB-s$0.000024 /$0.0000025per vCPU-s / GiB-s$0.000016per GB-sRAM bought per vCPU1,769 MBnot negotiableyour choice512 MiB to 32 GiB1,536 MBthe whole ceilingEffective $ per vCPU-second$0.0000288at 1,769 MB per vCPU$0.0000290at 2 GiB per vCPU$0.0000276at the 1,536 MB ceilingRequest charge$0.20 / M$0.40 / M$0.20 / MMonthly free grant400,000 GB-s+ 1 M requests180,000 vCPU-s+ 360,000 GiB-s, 2 M req400,000 GB-s+ 1 M executionsList prices in us-east-1, us-central1 and East US at the time of writing. The effective vCPU-second column is derived, not published, andmoves whenever a provider changes either of its two rates.
The three providers price a vCPU-second within 5% of each other. Any real difference in a tiling bill comes from the rows below the rate, not from the rate.

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

Cost of one 6,400-tile scene indexed to AWS Lambda at 3,008 MBFive horizontal bars indexing the cost of tiling the same 6,400-window scene. Cloud Run with 4 vCPU, 8 GiB and concurrency 8 costs 0.33 times the baseline at 56 cents. AWS Lambda at 3,008 MB is the 1.00 baseline at 1 dollar 69. Azure Functions Consumption at 1,536 MB is 1.05 at 1 dollar 77. Cloud Run with 1 vCPU and concurrency 1 is 1.24 at 2 dollars 09. AWS Lambda at 10,240 MB is 1.51 at 2 dollars 56.Same 6,400-tile scene, five configurationsCloud Run · 4 vCPU / 8 GiB · concurrency80.33x · $0.56AWS Lambda · 3,008 MB · baseline1.00x · $1.69Azure Functions · 1,536 MB Consumption1.05x · $1.77Cloud Run · 1 vCPU / 4 GiB · concurrency11.24x · $2.09AWS Lambda · 10,240 MB · concurrency 11.51x · $2.5601.6x the AWS Lambda 3,008 MB baselineAssumption set B: 40,960 x 40,960 px, 4 bands uint16, 512-px windows, same-region object storage, tuned range reads, us-east-1 /us-central1 / East US list prices at the time of writing. Orchestration, egress and log ingestion are excluded so the compute runtimes comparelike for like.
Cloud Run wins by 3x and loses by 24% with the same rate card — the only thing that changed between those two rows is --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.

python
# 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:

bash
python platform_cost_compare.py

Expected output:

code
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

When each platform is the right home for a tiling workloadThree panels listing the conditions that favour AWS Lambda, GCP Cloud Run and Azure Functions for raster tiling, followed by a verdict line stating that the deciding factor is the proportion of the tile spent waiting on object storage.Which runtime the workload is actually asking forAWS LambdaCPU-bound tiles under 15 minutesBursty, irregular scene arrivals —you pay nothing between themRetry granularity of one tilemattersSource data already in S3 in thesame regionYou want no scaling policy to ownGCP Cloud RunMore than half of each tile isnetwork waitWorking set above 10,240 MB, up to32 GiBA tile can exceed 15 minutes but not60The GDAL stack exceeds the 250MB unzipped package limitSteady throughput, so warminstances stay busyAzure FunctionsTiles fit inside 1,536 MB and 10minutesVolume stays inside the 400,000GB-second free grantSource data lives in Azure BlobStorage alreadyAbove that, Premium or ContainerApps, not ConsumptionNo path to more than about onevCPU on ConsumptionThe deciding number is the fraction of each tile spent waiting on object storage: above 50%, in-process concurrency beatsevery per-invocation runtime regardless of rate card.
Read the panel that matches your workload shape, not the one that matches your existing account. The wrong choice here is a 3x line item, which is larger than any tuning inside the handler will recover.

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

  • --concurrency above 1 needs a thread-safe handler. GDAL is thread-safe per dataset handle, not across them. Open a fresh rasterio dataset inside each request rather than sharing one at module scope, and size GDAL_CACHEMAX for 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 FunctionExecutionUnits is 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.


Back to Cost Modelling for Serverless Raster Pipelines