How to Configure 10 GB Memory for AWS Lambda Raster Processing
Set MemorySize to 10240 and EphemeralStorage.Size to 10240 in your Lambda function configuration. At this ceiling AWS allocates approximately 5.79 vCPUs — enabling parallel tile decoding, vectorized NumPy band math, and concurrent GDAL reprojection threads. Pair maximum memory with windowed I/O and explicit GDAL cache limits to prevent out-of-memory crashes that occur even at 10 GB when full rasters are loaded naively.
Context
The core tension in serverless raster processing is that Memory and CPU Allocation for Raster Workloads on AWS Lambda couples vCPU count directly to RAM tier: 1,769 MB per full vCPU. Below the 3,008 MB mark your function operates on a fraction of a single core, making CPU-bound operations like reprojection and band algebra painfully slow regardless of dataset size. At 10,240 MB you reach the current hard ceiling — roughly 6 vCPUs and network/disk I/O bandwidth that scales proportionally.
The failure mode that drives engineers to this configuration is predictable: a multi-band Sentinel-2 mosaic or a DEM hydrological conditioning job runs fine on a 3 GB memory tier for small tiles but OOMs or times out on production-scale inputs. Allocating 10 GB removes the compute bottleneck while Ephemeral Storage Limits in AWS Lambda — kept at the default 512 MB — silently starves GDAL’s disk cache and intermediate VRT staging. Both parameters must be raised together.
The diagram below shows how memory tier maps to vCPU allocation and where the 10 GB configuration sits relative to common raster workload sizes:
Prerequisites
Before raising the memory ceiling, confirm the following are in place:
- Runtime: Python 3.11 or 3.12. Earlier runtimes lack the improved
gcbehaviour needed for reliable C-extension deallocation. - IAM permission:
lambda:UpdateFunctionConfigurationon the target function ARN. Without this the AWS CLI and Terraform will silently fall back to current settings. - Environment variables set explicitly:
GDAL_DATA,PROJ_LIB, andLD_LIBRARY_PATHmust point to your Lambda layer paths. Never rely on defaults inside the execution environment. - Dependency versions:
rasterio>=1.3,numpy>=1.24,GDAL>=3.6. Earlier versions have known memory-leak paths in windowed write operations. - Service quota: Verify your account’s Lambda
function-and-layer-storageand per-function memory quota via the Service Quotas console. New accounts are sometimes capped below 10 GB until a quota increase is approved. - EphemeralStorage configured alongside MemorySize: As explained under Ephemeral Storage Limits in AWS Lambda, the
/tmppartition does not expand automatically when you raise RAM — it must be provisioned separately.
Implementation
The following snippets show the three most common configuration paths. After configuration, the Python handler demonstrates the windowed-I/O pattern that stabilises peak memory at 10 GB without OOM.
AWS CLI
aws lambda update-function-configuration \
--function-name raster-processor \
--memory-size 10240 \
--timeout 900 \
--ephemeral-storage '{"Size": 10240}' \
--environment 'Variables={
GDAL_CACHEMAX=2048,
GDAL_NUM_THREADS=4,
GDAL_DATA=/opt/share/gdal,
PROJ_LIB=/opt/share/proj,
LD_LIBRARY_PATH=/opt/lib,
GDAL_DISABLE_READDIR_ON_OPEN=EMPTY_DIR
}'
Terraform
resource "aws_lambda_function" "raster_processor" {
function_name = "raster-processor"
runtime = "python3.11"
handler = "handler.process_tile"
memory_size = 10240
timeout = 900
ephemeral_storage {
size = 10240
}
environment {
variables = {
# Hard limits: GDAL cache capped at 2 GB to leave headroom for numpy buffers
GDAL_CACHEMAX = "2048"
GDAL_NUM_THREADS = "4"
# Explicit paths — never rely on container defaults
GDAL_DATA = "/opt/share/gdal"
PROJ_LIB = "/opt/share/proj"
LD_LIBRARY_PATH = "/opt/lib"
GDAL_DISABLE_READDIR_ON_OPEN = "EMPTY_DIR"
}
}
}
AWS SAM
Resources:
RasterProcessor:
Type: AWS::Serverless::Function
Properties:
Runtime: python3.11
Handler: handler.process_tile
MemorySize: 10240
Timeout: 900
EphemeralStorage:
Size: 10240
Environment:
Variables:
GDAL_CACHEMAX: "2048"
GDAL_NUM_THREADS: "4"
GDAL_DATA: "/opt/share/gdal"
PROJ_LIB: "/opt/share/proj"
LD_LIBRARY_PATH: "/opt/lib"
GDAL_DISABLE_READDIR_ON_OPEN: "EMPTY_DIR"
Production handler with windowed I/O
import os
import gc
import uuid
import logging
import rasterio
from rasterio.windows import Window
import numpy as np
logger = logging.getLogger(__name__)
logger.setLevel(logging.INFO)
def _check_tmp_headroom(required_mb: int = 2048) -> None:
"""Abort early if /tmp does not have enough free space."""
stat = os.statvfs("/tmp")
free_mb = (stat.f_frsize * stat.f_bavail) / (1024 * 1024)
if free_mb < required_mb:
raise RuntimeError(
f"/tmp headroom too low: {free_mb:.0f} MB free, {required_mb} MB required"
)
def process_tile(event: dict, context) -> dict:
"""
Process a raster tile from S3 using windowed reads.
Requires MemorySize=10240, EphemeralStorage=10240, and explicit GDAL env vars.
"""
# Convert s3:// URI to the GDAL virtual filesystem path so rasterio
# streams byte ranges directly without a full download to /tmp.
src_path = event["s3_uri"].replace("s3://", "/vsis3/")
# Isolate each invocation under a unique subdirectory so warm-start
# container reuse does not corrupt previous outputs.
invocation_dir = f"/tmp/{uuid.uuid4().hex}"
os.makedirs(invocation_dir, exist_ok=True)
dst_path = os.path.join(invocation_dir, os.path.basename(src_path))
# Guard against /tmp exhaustion before writing any data.
_check_tmp_headroom(required_mb=2048)
window_size = 1024 # pixels per tile dimension; align with COG block size
with rasterio.open(src_path) as src:
profile = src.profile.copy()
profile.update(
dtype=rasterio.uint16,
count=1,
compress="deflate",
tiled=True,
blockxsize=512,
blockysize=512,
# Ensure GDAL writes the COG-compatible layout
interleave="band",
)
with rasterio.open(dst_path, "w", **profile) as dst:
for row_off in range(0, src.height, window_size):
for col_off in range(0, src.width, window_size):
width = min(window_size, src.width - col_off)
height = min(window_size, src.height - row_off)
window = Window(col_off, row_off, width, height)
# Read only the current 1024×1024 window — not the full raster.
# At 10240 MB, ~6 vCPUs decode tiles in parallel across bands.
data = src.read(window=window)
# Band normalisation: scale uint16 to 0-100 integer range
processed = np.clip(
(data.astype(np.float32) / 65535.0 * 100),
0, 100
).astype(np.uint16)
dst.write(processed, window=window)
# Explicit deallocation releases C-extension buffers before
# gc runs automatically — critical for back-to-back windows.
del data, processed
gc.collect()
logger.info("Processed %s → %s", src_path, dst_path)
return {"statusCode": 200, "output_path": dst_path}
Verification
Run the following after deployment to confirm the memory tier is active and peak usage is within bounds:
# Invoke the function with a representative test event and capture the log output
aws lambda invoke \
--function-name raster-processor \
--cli-binary-format raw-in-base64-out \
--payload '{"s3_uri":"s3://your-bucket/test-tile.tif"}' \
--log-type Tail \
response.json \
--query 'LogResult' --output text | base64 --decode
Expected output excerpt:
REPORT RequestId: … Duration: 4321.00 ms Billed Duration: 4400 ms
Memory Size: 10240 MB Max Memory Used: 3847 MB Init Duration: 2100.00 ms
Max Memory Used should stay below 8,000 MB under normal raster workloads. If it regularly exceeds 9,000 MB, reduce the window_size from 1024 to 512 pixels per dimension and verify GDAL_CACHEMAX is honoured:
import os
from osgeo import gdal
# Confirm GDAL sees the cache cap at runtime
cache_mb = gdal.GetCacheMax() / (1024 * 1024)
print(f"GDAL cache cap: {cache_mb:.0f} MB") # Expected: 2048 MB
assert cache_mb == 2048, f"GDAL_CACHEMAX not applied — got {cache_mb} MB"
To query CloudWatch directly:
aws cloudwatch get-metric-statistics \
--namespace AWS/Lambda \
--metric-name MaxMemoryUsed \
--dimensions Name=FunctionName,Value=raster-processor \
--start-time "$(date -u -d '-1 hour' '+%Y-%m-%dT%H:%M:%SZ')" \
--end-time "$(date -u '+%Y-%m-%dT%H:%M:%SZ')" \
--period 300 \
--statistics Maximum \
--output table
Gotchas and Edge Cases
-
GDAL_NUM_THREADS=ALL_CPUScauses thread contention. Lambda’s execution environment shares host-level CPU resources. Pinning to an explicit integer (4 for 10 GB, 2 for tiers below 7 GB) prevents the thread pool from spawning more workers than vCPUs actually available to your container slice. -
Warm-start
/tmpaccumulation. Lambda reuses containers across invocations. If your handler writes to a fixed/tmp/output.tifpath, a second invocation on the same container can inherit stale data. Use theuuid.uuid4().hexsubdirectory pattern shown above and delete the directory at the end of the handler. -
InitDurationspikes at 10 GB. Cold-start overhead increases slightly at higher memory tiers because AWS provisions a larger execution environment. The Cold Start Mapping for Python GDAL sequence shows that GDAL shared-library resolution accounts for 60–80% ofInitDuration. Use Lambda Layers with pre-built GDAL binaries and enable Provisioned Concurrency for latency-sensitive pipelines. -
Service quota is per-region. A quota increase in
us-east-1does not propagate toeu-west-1. If you deploy to multiple regions, raise the quota in each independently via the AWS Service Quotas console before applying Terraform.
Frequently Asked Questions
Does raising MemorySize increase my per-invocation cost? Yes. Lambda billing is Duration × MemorySize in GB-seconds. At 10 GB, a 5-second invocation costs 50 GB-seconds versus 25 GB-seconds at 5 GB. The cost-per-tile crossover depends on how much the extra vCPUs reduce duration. For CPU-bound reprojection jobs, measured speedup typically reaches 2–3× at the 10 GB tier, making the net cost flat or slightly lower despite the higher per-ms rate. For I/O-bound workloads (streaming COGs from S3), memory above 5–6 GB rarely improves duration enough to justify the cost.
Can I use multiprocessing instead of windowed reads to saturate the 6 vCPUs?
Avoid multiprocessing.Pool in Lambda. Forking inside the execution environment is unreliable — child processes inherit file descriptors, GDAL drivers, and PROJ context that were initialised for the parent, leading to silent corruption. Use concurrent.futures.ThreadPoolExecutor instead; rasterio releases the GIL during file I/O, allowing multiple threads to download and decode windows concurrently across the available vCPUs.
Related
- Memory and CPU Allocation for Raster Workloads — sizing strategy, platform comparisons, and vCPU-to-memory ratios
- Ephemeral Storage Limits in AWS Lambda — configuring
/tmpindependently of RAM for GeoTIFF staging - Managing /tmp Storage Limits for GeoTIFF Extraction — windowed extraction pattern that avoids materialising rasters on disk
- Cold Start Mapping for Python GDAL —
InitDurationprofiling and Provisioned Concurrency strategy for heavy GDAL layers - Reducing Python GDAL Cold Starts with Provisioned Concurrency — concrete Lambda configuration to eliminate cold-start latency at the 10 GB tier
Back to Serverless Geospatial Architecture & Platform Limits