Skip to content

Cold Start Comparison: AWS vs GCP vs Azure for Python GDAL

A Python GDAL/rasterio function pays a 3-8 second cold-start penalty on every new execution environment, and that number is remarkably consistent across AWS Lambda, GCP Cloud Functions 2nd gen, and Azure Functions because the dominant cost — resolving libgdal, libproj, and libgeos, loading proj.db, and running GDAL’s driver registration sweep — is identical on all three. Where the platforms genuinely diverge is in the envelope around that import: how the container is provisioned, how dependencies are mounted, and what each one charges to keep an environment warm. This page sits under Serverless Geospatial Architecture & Platform Limits and compares all three head-to-head so you can choose a warm-keeping strategy with the real numbers in front of you.

Why Cold-Start Parity Matters for Geospatial Workloads

Most serverless latency guidance assumes a lightweight handler that imports boto3 and a JSON parser. Geospatial functions break that assumption. A single import rasterio triggers a chain that loads dozens of shared objects and opens the PROJ SQLite database before your first line of business logic runs — the same chain documented in the cold start mapping for Python GDAL sequence. That makes the GDAL initialization cost the floor beneath any cross-cloud comparison, and it is why raw import timings cluster so tightly regardless of provider.

The consequence for architecture is that you cannot pick a platform on cold-start speed alone; the import floor swamps the differences. Instead the decision hinges on three things the platforms do control: the container startup model, whether dependencies ship as a mounted layer or baked into an image, and the pricing of the warm-keeping mechanism. A map-tile endpoint that fans out per-tile is a different problem from a nightly DEM mosaicking job that runs against the timeout ceiling — the first cannot tolerate a 5-second stall, the second happily does. Memory allocation compounds this: because CPU scales with memory on both AWS and GCP, an under-provisioned function initializes GDAL more slowly, so the memory and CPU allocation for raster workloads choice directly changes your cold-start number.

It also matters that the three clouds deliver dependencies through fundamentally different mechanisms, and that mechanism is what the pre-registration phases of the diagram below actually represent. AWS mounts a Lambda Layer at /opt and extracts it into the sandbox, so an oversized layer inflates the extract phase; the discipline of stripping unnecessary Python packages pays back directly at every cold start. GCP has no layer concept — the whole runtime is a container image pulled from Artifact Registry, so a bloated image is felt on every scale-from-zero event. Azure Consumption mounts dependencies from a share and starts a separate Python worker process after the host, which is the structural reason its numbers sit a couple of seconds higher. Understanding which phase your platform pays for tells you where optimization effort belongs before you reach for a warm-keeping mechanism at all.

Python GDAL cold-start phases across three clouds Three horizontal timelines. AWS Lambda: sandbox provision, layer extract, shared-library resolution, GDAL driver registration. GCP Cloud Functions 2nd gen: image pull, container boot, shared-library resolution, GDAL driver registration. Azure Functions Consumption: host start, Python worker init, dependency mount, GDAL driver registration. The GDAL registration segment is the largest on all three. AWS Lambda sandbox init layer extract .so resolution GDAL driver registration GCP CF 2nd gen image pull boot .so resolution GDAL driver registration Azure Consumption host start worker init dependency mount GDAL driver registration 0 s elapsed init time ~8 s
The GDAL driver-registration segment dominates the init budget on every platform; the differences live in the phases before it.

Platform-by-Platform Limits

The table below captures the constraints that shape cold-start behaviour for an identical rasterio + pyproj + shapely stack. Quotas that bound the deployment surface — and therefore the extract/pull cost — are included alongside the warm-keeping controls.

Cold-start factor AWS Lambda GCP Cloud Functions (2nd gen) Azure Functions (Consumption)
Typical Python GDAL cold start 3-6 s 3-6 s (image-size dependent) 5-9 s
Startup model Firecracker microVM + layer mount Cloud Run container (image pull) App-host process + Python worker
Dependency delivery Lambda Layers (/opt, 250 MB unzipped, 5 max) Container image (Artifact Registry) Zip deploy / mounted share
Max memory 10,240 MB 32,768 MB 1,536 MB
Max timeout 15 min 60 min 10 min
Default concurrency 1,000 (regional, soft) 3,000 (per project) 200 (per function)
Warm-keeping mechanism Provisioned concurrency Minimum instances (Cloud Run) Premium always-ready instances
Warm-keeping billing Per GB-s of provisioned capacity Per idle instance-second Per always-ready instance (Premium)
CPU during init Scales with memory Scales with configured vCPU Fixed per plan

Two structural differences stand out. First, GCP’s cold start is governed by image pull time, so a bloated container hurts it more than an oversized layer hurts AWS — the multi-stage Dockerfile for GDAL on Cloud Run discipline is what keeps that number low. Second, Azure Consumption cannot warm-keep at all; the moment you need pre-allocated instances you are on the Premium plan, which changes the pricing conversation entirely.

Step-by-Step Implementation

Step 1: Establish a Comparable Baseline

Measure the raw import cost on each platform with the same handler, isolating GDAL registration from the surrounding phases. Always set the GDAL environment variables explicitly — omitting them forces GDAL onto compile-time paths that do not exist in any of the three runtimes.

python
# cold_start_probe.py — deploy the same file to all three clouds
import os, time

# Point GDAL and PROJ at the packaged data dirs. Paths differ per platform:
#   AWS Lambda layer -> /opt/share, GCP/Azure container image -> /usr/share
_BASE = os.environ.get("GEO_DATA_BASE", "/opt/share")
os.environ.setdefault("GDAL_DATA", f"{_BASE}/gdal")
os.environ.setdefault("PROJ_LIB", f"{_BASE}/proj")
os.environ.setdefault("LD_LIBRARY_PATH", "/opt/lib:" + os.environ.get("LD_LIBRARY_PATH", ""))

_COLD = True  # module-level flag: True only on a fresh execution environment

def handler(event, context):
    global _COLD
    t0 = time.perf_counter()
    from osgeo import gdal            # shared-library resolution happens here
    t1 = time.perf_counter()
    gdal.AllRegister()                # driver registration sweep — the expensive step
    t2 = time.perf_counter()
    was_cold, _COLD = _COLD, False
    return {
        "cold": was_cold,
        "import_ms": round((t1 - t0) * 1000, 1),
        "register_ms": round((t2 - t1) * 1000, 1),
        "gdal": gdal.VersionInfo("RELEASE_NAME"),
    }

Step 2: Configure AWS Provisioned Concurrency

On AWS, provisioned concurrency keeps a fixed number of environments initialized past the import rasterio point. Pin it to a published version, never $LATEST.

bash
aws lambda put-provisioned-concurrency-config \
  --function-name geo-tiler \
  --qualifier 7 \
  --provisioned-concurrent-executions 5

The reducing Python GDAL cold starts with provisioned concurrency guide covers the init-time trade-offs and the auto-scaling schedule that trims idle cost outside peak hours.

Step 3: Configure GCP Minimum Instances

GCP Cloud Functions 2nd gen runs on Cloud Run, so warm-keeping is a --min-instances value on the underlying service. This holds N containers past their GDAL init.

bash
gcloud functions deploy geo-tiler \
  --gen2 --runtime=python311 --region=us-central1 \
  --memory=4Gi --cpu=2 --min-instances=2 --max-instances=50 \
  --set-env-vars=GEO_DATA_BASE=/usr/share \
  --trigger-http

The dedicated reducing GCP Cloud Functions 2nd gen cold starts with minimum instances walkthrough works through the idle-cost math and the Terraform form of this setting.

Step 4: Configure Azure Always-Ready Instances

Azure only offers pre-warmed capacity on the Premium (Elastic) plan via always-ready instances; Consumption cannot do it.

bash
az functionapp plan create \
  --name geo-premium-plan --resource-group geo-rg \
  --location eastus --sku EP1 --min-instances 1

az functionapp update \
  --name geo-tiler --resource-group geo-rg \
  --set siteConfig.minimumElasticInstanceCount=1

Measurement and Verification

Invoke the probe twice per platform: the first call after a deploy reports "cold": true with the real init split, the second reports "cold": false. A representative result on a well-sized AWS function:

json
{"cold": true, "import_ms": 910.4, "register_ms": 3120.7, "gdal": "3.9.0"}

The register_ms value is the number to watch — anything above ~4 s across all three clouds points at an oversized deployment or dynamic-linking overhead rather than a platform difference. Confirm the platform-reported metric too: CloudWatch Init Duration (AWS), the Startup latency field in Cloud Run request logs (GCP), and the Cold Start dimension in Application Insights (Azure). When those numbers stay flat and only your warmed invocations count, the warm-keeping mechanism is working.

Failure Modes and Debugging

Provisioned concurrency shows no improvement (AWS): You pinned the config to $LATEST or an alias that moved. Provisioned concurrency only pre-warms a specific published version — verify the --qualifier matches the deployed version and that ProvisionedConcurrencyUtilization is non-zero in CloudWatch.

GCP cold start dominated by image pull: The container image is hundreds of MB because build tooling leaked into the runtime stage. Rebuild with a multi-stage Dockerfile and confirm the final image is lean; a 1.5 GB image can add several seconds of pull time to every scale-from-zero event.

Azure function still cold despite Premium plan: minimumElasticInstanceCount was left at 0, or the app is still on the Consumption plan. Always-ready instances require both an Elastic Premium plan and a non-zero minimum count.

register_ms spikes only under load: Memory-linked CPU throttling. On AWS and GCP, initialization runs slower at low memory tiers because vCPU is proportional — raise the memory/CPU allocation and re-measure.

Cold start fine, first real request slow: GDAL registered but proj.db was not memory-resident, so the first coordinate transform pays a disk read. Warm the transform inside the init path, not the handler.

Cost and Scaling Considerations

Warm-keeping is pure idle spend, and the three clouds meter it differently. On AWS, provisioned concurrency bills per GB-second of reserved capacity whether or not a request arrives — five environments at 4 GB held around the clock is a fixed monthly line item you can shrink with a scheduled scaling policy. On GCP, minimum instances bill per idle instance-second at the Cloud Run rate, which is why a --min-instances=2 on a 4 GiB/2 vCPU function has a predictable but non-trivial floor. On Azure, moving from Consumption to Premium to get always-ready instances changes the entire cost basis: you leave the pay-per-execution model for a plan with a standing per-instance charge.

The decision rule is workload shape. Interactive tiling behind a viewer justifies warm capacity because a 5-second GDAL stall is user-visible; the 10 GB GeoTIFF processing recipe and other batch pipelines do not, because a cold start is a rounding error against a multi-minute job. For queue-driven fan-out, tune the SQS and Pub/Sub queue routing concurrency so a burst does not force hundreds of simultaneous cold starts in the first place — often cheaper than paying to keep them all warm.

There is also a middle path that costs nothing per hour: attack the import floor itself so that even the cold starts you do pay for are shorter. Shrinking the deployment surface — a stripped, statically-linked GDAL stack rather than a fat wheel bundle — cuts the extract or pull phase on all three clouds. Lazy-importing rarely used drivers, setting GDAL_DATA and PROJ_LIB explicitly so GDAL does not probe non-existent paths, and disabling the .aux.xml sidecar with GDAL_PAM_ENABLED=NO each trim tens to hundreds of milliseconds off the registration sweep. On workloads with a modest, predictable request rate, these no-cost optimizations can pull the cold start low enough that warm-keeping stops being worth the idle spend entirely. Measure first: if register_ms already dominates and cannot be reduced, warm-keeping is the only remaining lever and the cost comparison above becomes the real decision. Combine warm-keeping with a lean package rather than treating them as alternatives — a small warm pool over a fast-initializing image is both cheaper and more resilient to bursts than a large warm pool papering over a slow one.

Frequently Asked Questions

Which cloud has the fastest cold start for a Python GDAL function?

For an identical rasterio stack, AWS Lambda and GCP Cloud Functions 2nd gen land in a similar 3-6 second band once the deployment is sized well, while Azure Functions on Consumption is typically slower (5-9 seconds) because the Python worker and dependency mount initialize after the host process. GCP pulls a container image, so image size dominates its number more than layer size does on AWS.

Why is the cold start 3-8 seconds even on a warm-friendly runtime?

The GDAL stack must resolve libgdal, libproj, and libgeos, load proj.db, and register every driver before your handler runs. That work is CPU and I/O bound and happens once per new execution environment regardless of provider, which is why the raw import cost is comparable across all three clouds.

What is each platform’s warm-keeping mechanism called?

AWS Lambda calls it provisioned concurrency, GCP Cloud Functions 2nd gen exposes it as minimum instances on the underlying Cloud Run service, and Azure Functions offers always-ready instances only on the Premium (Elastic) plan. All three bill for idle capacity.

Is provisioned concurrency worth it for bursty geospatial workloads?

For latency-sensitive interactive tiling it usually is, because a 4-6 second GDAL cold start is unacceptable behind a map viewer. For nightly batch reprojection it usually is not, because the jobs tolerate cold starts and idle warm instances would burn money between runs.


Back to Serverless Geospatial Architecture & Platform Limits