Distributed Processing on Coiled & AWS Batch

When a raster workload outgrows a single machine, the question is no longer how to compute an index but where the compute should live and how it reaches the data. This guide contrasts two production deployment models for running Python raster pipelines in the cloud: Coiled, which spins up managed Dask clusters directly from Python, and AWS Batch, which runs containerized jobs against a managed compute queue. Both keep compute next to the data and write results back to object storage; they differ in execution shape, ergonomics, and cost profile. For the broader orchestration context, see the parent section on Cloud Execution & Orchestration.

The audience here is remote sensing analysts and data engineers who already have a working local pipeline and now need it to run over hundreds or thousands of scenes without saturating a laptop’s memory or bandwidth. The two hands-on guides in this section walk through each model end to end; this page gives you the decision framework and the shared infrastructure concerns that apply to both.


Two deployment models at a glance

Coiled and AWS Batch solve the same problem from opposite directions. Coiled gives you a live, elastic Dask scheduler you talk to from a notebook or script — ideal when the computation is one large distributed graph over an xarray cube. AWS Batch gives you a queue that runs an image to completion per job — ideal when the work is naturally embarrassingly parallel, one container invocation per scene. The diagram below places both against the same data plane.

Coiled Dask cluster versus AWS Batch, sharing a same-region S3 data plane Two deployment models drawn side by side. On the left, a Python client calls coiled.Cluster, which provisions a scheduler and adaptive workers that read COGs from S3 and stream results back. On the right, a boto3 submit_job call enqueues array jobs into an AWS Batch queue, which runs one container per scene against the same S3 bucket. Both compute planes sit in the same region as the data. Coiled — managed Dask Python client / notebook coiled.Cluster(...) + Client Scheduler task graph Adaptive workers AWS Batch — containers boto3 submit_job() array job over N scenes Job queue → compute environment task 0 task 1 task N Same-region S3 bucket (COG archive) HTTP range reads in · index results written back out near-zero egress · IAM-scoped access Both models: keep compute in the data's region, read COGs by range, write results back

Prerequisites

Both models sit on the same rasterio/GDAL core; you additionally install the Coiled or AWS SDK depending on which you drive. Pin the geospatial stack so it matches whatever runs on the workers or inside the container — GDAL and PROJ drift is the top cause of silent coordinate errors on remote compute.

# Shared core
pip install "rasterio>=1.3" "numpy>=1.24"
# Coiled path
pip install "coiled>=1.0" "dask>=2024.1" "distributed>=2024.1" "stackstac>=0.5"
# AWS Batch path (local tooling to submit jobs)
pip install "boto3>=1.34"
Library / tool Min version Model Why required
rasterio 1.3 Both COG range reads via GDAL /vsis3/, CRS parsing
coiled 1.0 Coiled Provision and manage Dask clusters from Python
dask / distributed 2024.1 Coiled Scheduler + Client that the Dask cluster runs
stackstac 0.5 Coiled Stream a STAC collection into a lazy xarray cube
boto3 1.34 AWS Batch Register job definitions and submit array jobs
Docker 24+ AWS Batch Build the immutable per-scene job image

Conceptual prerequisites: you should already have a working single-machine pipeline and be comfortable with Dask chunking and with reading assets from a STAC catalog. An AWS account with S3, IAM, and (for Batch) ECR access is assumed; a Coiled account is assumed for the managed-cluster path.


Key components

Component Model Role
coiled.Cluster Coiled Provisions a Dask scheduler + workers as cloud VMs from Python; supports n_workers, worker_memory, and adaptive scaling
distributed.Client Coiled Connects your session to the Dask cluster scheduler so .compute() runs on the cloud
Software environment Coiled Named, cached dependency set (or synced local env) that guarantees worker/client package parity
Job definition AWS Batch Declares the container image, vCPU/memory, command, and IAM job role for a class of jobs
Array job AWS Batch A single submit_job that fans out into N indexed child tasks — one per scene
Compute environment AWS Batch The managed pool of EC2 (or Fargate) capacity the queue schedules onto
Docker image AWS Batch Immutable bundle of the rasterio/GDAL stack and job script, pinned for reproducibility
S3 + IAM Both The shared data plane: range-read COGs in, write index outputs back, scoped by role

Head-to-head

Dimension Coiled (managed Dask) AWS Batch (containers)
Execution shape One large distributed graph Many independent per-scene jobs
Interactivity Live scheduler, notebook-friendly Fire-and-forget queue
State Shared cluster memory across tasks Isolated per-container
Scaling Adaptive up/down during a session Queue depth drives EC2 scale-out
Env parity Software environment / package sync Baked into the Docker image
Best for STAC-to-xarray cubes, composites Scene-at-a-time transforms at scale
Cost lever Close cluster when idle Spot compute + right-sized jobs
Cold start Seconds to a few minutes Image pull + provisioning per wave

Neither model is strictly better. A mature deployment often uses both: Coiled for the interactive research loop where you design a composite over a STAC collection, then AWS Batch to run the finalized script over the full archive.


When to choose Coiled

Choose Coiled when the computation is a single connected Dask graph and you want to iterate on it. Streaming a STAC search into an xarray cube, computing a cloud-free composite, and inspecting the result interactively is the canonical fit. The Dask cluster holds intermediate results in distributed memory, so re-running a slightly changed reduction does not re-read every COG from scratch.

import coiled
from distributed import Client

# Provision a right-sized Dask cluster in the data's region
cluster = coiled.Cluster(
    name="stac-composite",
    n_workers=8,
    worker_memory="16GiB",
    region="us-west-2",              # same region as the COG bucket
    software="rs-stac-2026",         # pinned software environment
)
client = Client(cluster)             # route all .compute() calls here
print(client.dashboard_link)

From here you would stackstac a STAC item collection into a lazy xarray cube and reduce it — the full walkthrough lives in Launching a Coiled Cluster for STAC Processing. Because the Dask cluster is elastic, adaptive scaling can grow workers while a large .compute() runs and release them afterward. Always close the Dask cluster when the session ends so idle VMs stop billing.


When to choose AWS Batch

Choose AWS Batch when the work is naturally one job per scene and each job runs a fixed script to completion. There is no shared state to exploit, so a live scheduler adds nothing; a container image and a queue fit better. The array job primitive lets a single submission fan out over thousands of scenes, and the queue scales the underlying EC2 fleet to match depth.

import boto3

batch = boto3.client("batch", region_name="us-west-2")

# One array job → N child tasks, each processes a different scene by index
response = batch.submit_job(
    jobName="cog-index-run",
    jobQueue="raster-queue",
    jobDefinition="cog-processor:3",   # versioned job definition
    arrayProperties={"size": 500},     # 500 scenes, 500 child tasks
)
print(response["jobId"])

Each child task reads its scene’s COG from S3, runs the transform in-container, and writes the result back — details in Processing COGs on AWS Batch with Docker. Batch pairs naturally with EC2 Spot compute environments, which cut cost substantially for interruption-tolerant, idempotent per-scene work.


Shared infrastructure concerns

Both models succeed or fail on the same three fundamentals.

Same-region compute

Placing workers in the same region as the bucket is the single highest-leverage decision. A Cloud-Optimized GeoTIFF pipeline issues many small HTTP range reads per file; cross-region, each pays egress and tens of milliseconds of latency, and it compounds across thousands of files. Set region on the Coiled cluster and the jobQueue compute environment to match the data. Verify the bucket’s region first:

import boto3

s3 = boto3.client("s3")
loc = s3.get_bucket_location(Bucket="sentinel-cogs")["LocationConstraint"]
print(loc or "us-east-1")   # None → us-east-1; pin compute to this region

VM sizing

Size VMs to the per-task memory footprint, not to a round number of cores. For raster work the binding constraint is memory per worker: a single Sentinel-2 scene read into float32 across several bands can hold a few gigabytes resident. On Coiled set worker_memory above the largest chunk a worker holds at once; on Batch set the job definition’s memory above the peak resident set of one scene, with headroom for GDAL’s block cache.

Software / package environment parity

Workers must run the same GDAL, rasterio, and PROJ versions you tested locally. GDAL or PROJ version drift silently changes coordinate transforms and resampling output. On Coiled, build a named software environment from pinned requirements; on Batch, pin exact versions in the Docker image. Never rely on latest tags for either — an image rebuilt months later can pull a newer GDAL and change your results.


Verification & testing

Before committing a full run, prove three things on a single scene: the worker environment matches, the data is local, and results write back correctly.

import rasterio

# Run this ON a worker (Coiled) or inside the container (Batch) to confirm parity
def assert_geo_stack(expected_gdal: str) -> None:
    got = rasterio.__gdal_version__
    assert got == expected_gdal, f"GDAL drift: worker {got} != pinned {expected_gdal}"
    # Confirm a header-only remote open succeeds from the compute environment
    url = "https://sentinel-cogs.s3.us-west-2.amazonaws.com/sentinel-s2-l2a-cogs/36/N/YF/2023/6/S2A_36NYF_20230615_0_L2A/B04.tif"
    with rasterio.open(url) as src:
        assert src.crs is not None, "remote COG opened but CRS is missing"
    print("environment parity + remote read OK")

On Coiled, submit this with client.submit(assert_geo_stack, "3.9.2").result() so it executes on a worker, not your laptop. On Batch, run it as the container entrypoint of a one-task test job and read the result from CloudWatch logs. Confirm a written output object appears in S3 with the expected size and CRS before scaling to the full archive.


Troubleshooting

ModuleNotFoundError on a Coiled worker but not locally — the worker software environment does not match your session. Rebuild the environment from the same pinned requirements, or enable package sync, and confirm the Dask cluster picked up the new software= name. Client/worker mismatch also surfaces as pickle/deserialization errors.

AWS Batch task stuck in RUNNABLE — the compute environment cannot place the job. The requested vCPU/memory exceeds any instance type in the environment, the environment is at its maxvCpus ceiling, or the subnet has no capacity. Lower the job definition’s resource request or raise the compute environment limits.

AccessDenied reading or writing S3 from a job — the job role lacks the right IAM permissions. The Batch job role (not the execution role) needs s3:GetObject on the input prefix and s3:PutObject on the output prefix. On Coiled, attach the equivalent instance profile or pass credentials to workers.

High cost with low utilization — an idle Coiled cluster left open bills for its VMs, and oversized Batch jobs waste reserved memory. Close clusters at session end, enable adaptive scaling, and right-size Batch memory to the measured peak. See Optimizing Pipeline Cost and Performance for the full cost model.

Cross-region slowness — reads are slow and egress charges appear even though the pipeline “works”. Compute is in a different region than the bucket. Move the Dask cluster/queue to the data’s region rather than moving the data.


FAQ

When should I use Coiled instead of AWS Batch for raster processing?

Use Coiled when the work is an interactive or exploratory Dask computation — streaming a STAC stack into an xarray cube, iterating on a composite, and inspecting results in a notebook. Reach for AWS Batch when you have thousands of independent per-scene jobs that each run a fixed script to completion, where an array job and a container image fit the shape of the work better than a live scheduler.

Why does same-region compute matter so much for cloud raster jobs?

Cloud providers charge egress when bytes leave a region and add latency on every cross-region range read. A COG pipeline issues many small HTTP range requests per file, so placing workers in the same region as the bucket keeps reads on the internal network — near-zero egress cost and single-digit-millisecond latency instead of tens of milliseconds per request.

How do I keep the worker environment identical to my local environment?

On Coiled, build a named software environment from the same pinned requirements you run locally, or let package sync mirror your active environment. On AWS Batch, bake dependencies into the Docker image and pin exact versions so the GDAL, rasterio, and PROJ stack on the worker matches what you tested. Version drift in GDAL or PROJ is the most common source of silent coordinate errors on remote workers.


Deep-Dive Articles