Scaling Geospatial Pipelines: From 10 Files to 10,000 in AWS Batch
The production lessons behind moving an imagery workflow from local scripts to an observable, permission-safe, highly parallel cloud pipeline.

- tiles per run
- 10 → 10,000
- memory per job
- 4–8 GB
- array-job ceiling
- 10,000
- case-study quota target
- 1,000 vCPUs
Scaling a geospatial pipeline is rarely only a coding problem. It is an infrastructure problem involving memory, identity, concurrency, observability, and cost. This is the behind-the-scenes architecture we used to make all five behave as one production system.
01. Treat “Essential container exited” as a starting point
Our first AWS Batch runs failed with the unhelpfully broad Essential container in task exited message. The useful signal lived one layer deeper in the exit code and container reason.
Exit code 137
The kernel killed the process
Raster operations can expand compressed imagery into large in-memory arrays. If the job reaches its memory ceiling, the operating system can kill it immediately. Profile representative tiles, then raise the job definition from 2 GB to 4 or 8 GB only when the measurements justify it.
Exit code 1
The application crashed
In our case Python failed before buffered logs reached CloudWatch. The job looked silent even though the process had already found a missing file and exited.
We made Python logs stream immediately from the container:
# Dockerfile
ENV PYTHONUNBUFFERED=1
# Equivalent runtime option
CMD ["python", "-u", "main.py"]02. Give the platform and the workload separate identities
A headless container does not inherit your laptop credentials. Our NoCredentialsError disappeared once we separated the permissions required to launch a task from the permissions required by the code inside it.
Execution role
Runs the container
- Pulls the image from Amazon ECR
- Writes container logs to CloudWatch Logs
- Retrieves referenced secrets when configured
Job role
Runs the application
- Reads only the required S3 input prefixes
- Writes only the designated output prefixes
- Uses least-privilege access for any other AWS API
AWS documents the same separation: the execution role is for the ECS/Fargate agent, while the task or job role is for application calls to services such as S3. Review the AWS IAM role guide.
03. Use one array job to coordinate 10,000 independent tiles
Once a single tile completed reliably, the workload became naturally parallel: every child job could use its own AWS_BATCH_JOB_ARRAY_INDEX to select one record from a validated manifest.
aws batch submit-job \
--job-name imagery-production \
--job-queue geospatial-prod \
--job-definition imagery-worker:12 \
--array-properties size=10000AWS Batch supports array sizes from 2 to 10,000. The parent job becomes the management handle while each index is scheduled as a separate child job. That is ideal for tile processing because failures and retries stay isolated to individual inputs. See the official array-job behavior.
04. Scale compute deliberately—not all at once
Submitting 10,000 jobs does not mean 10,000 should start simultaneously. In our environment, the initial 32-vCPU service quota limited throughput, so we requested a case-specific increase to 1,000 vCPUs and kept the compute environment bounded by cost and downstream capacity.
We also smoothed the first S3 request from every container with bounded jitter and SDK retries. Jitter is not a replacement for proper prefix design or exponential backoff; it simply avoids an artificial burst caused by thousands of identical startup paths.
import os
import random
import time
index = int(os.environ["AWS_BATCH_JOB_ARRAY_INDEX"])
time.sleep(random.uniform(0, 180)) # smooth the startup wave
process_tile(manifest[index])Quota
Set maximum vCPUs to a measured throughput and budget target.
Storage
Distribute hot request paths and keep compute in the same Region as S3.
Retries
Use SDK retry behavior with exponential backoff and monitor 503 responses.
Amazon S3 scales to high request rates, but scaling is gradual and brief 503 Slow Down responses can occur during a sharp ramp. Read the current S3 performance guidance.
05. Make geospatial functions explicit and testable
Infrastructure stability exposed the next failure: a TypeError around Rasterio's rasterize() call. We removed positional ambiguity by switching to explicit keyword arguments and making the output geometry part of the log context.
from rasterio.features import rasterize
mask = rasterize(
shapes=features,
out_shape=(height, width),
transform=transform,
fill=0,
default_value=1,
dtype="uint8",
)Keyword arguments do not become safer only because concurrency is high; they make the call contract clearer across library upgrades and easier to diagnose when thousands of jobs execute the same code path.
06. The final production workflow
| Step | Action | Outcome |
|---|---|---|
| Debug | Forced Python logs to stdout | Found the missing file and fixed the Docker build context. |
| Permissions | Assigned execution and job roles | Resolved S3 authentication with explicit workload identity. |
| Logic | Made rasterize arguments explicit | Stabilized the processing function and its diagnostics. |
| Scale | Raised the bounded compute quota | Enabled high concurrency without unlimited spend. |
| Operate | Added metrics, retries, and per-tile logs | Made failures observable and independently recoverable. |
Start small
Prove ten tiles before you submit ten thousand.
Run a representative smoke test, make IAM permissions explicit, confirm memory headroom, verify deterministic output keys, and watch billing metrics from day one. Scale only after the system explains its own failures.
Planning a larger run?
Design the failure path before the scale test.
We help teams turn fragile geospatial scripts into observable cloud pipelines with clear resource, security, and cost boundaries.