Skip to content

Ephemeral Storage Limits in AWS Lambda for Geospatial Processing

AWS Lambda mounts an NVMe-backed /tmp volume with a configurable ceiling of 10,240 MB (10 GB), up from the historical 512 MB default. For geospatial workloads, sizing this correctly is non-negotiable: a single 2 GB compressed GeoTIFF routinely expands to 6–8 GB of working files once GDAL decompresses blocks, writes VRT intermediates, and fills its block cache. Undershoot the allocation and the function terminates mid-transform with a DiskSpaceExhausted error; overshoot and you pay for storage you never touch.

Why This Constraint Matters for Geospatial Workloads

Geospatial formats are inherently I/O-heavy. A LZW-compressed GeoTIFF holding a single Sentinel-2 granule (110 × 110 km at 10 m resolution) weighs roughly 700 MB on disk but expands to 1.8–2.5 GB when GDAL decompresses it for processing. Add a VRT wrapper, an output draft file, and GDAL’s block cache and the total /tmp footprint easily crosses 5 GB for a single invocation. Common scenarios and their scratch footprints:

Workload Compressed input Peak /tmp usage
Single Sentinel-2 band reproject 200 MB 800 MB – 1.2 GB
Full 13-band S2 granule reproject 700 MB 3–5 GB
LiDAR LAZ → DEM (pdal) 400 MB 1.5–3 GB
GeoPackage 500k features to vector tiles 300 MB 1–2 GB
10 GB orthomosaic tiling 10 GB 20–30 GB (must chunk or offload)

For the orthomosaic case, even 10 GB /tmp is insufficient in a single Lambda invocation — that workload belongs in a batch vs. streaming split or a step-function workflow that shards the mosaic before dispatching tiles to individual functions.

Peak ephemeral storage per invocation for five geospatial workloadsHorizontal bar chart of peak slash tmp usage for five workloads: a single Sentinel-2 band reproject at 1,200 MB, a full thirteen-band Sentinel-2 granule reproject at 5,000 MB, a LiDAR LAZ to DEM conversion at 3,000 MB, a GeoPackage of 500,000 features converted to vector tiles at 2,000 MB, and a 10 GB orthomosaic tiling job at 30,000 MB, which is nearly three times the 10,240 MB Lambda ceiling.Peak /tmp for a single invocation, by workloadSingle Sentinel-2 band reproject1,200 MBFull 13-band S2 granule reproject5,000 MBLiDAR LAZ to DEM (PDAL)3,000 MBGeoPackage 500k features to tiles2,000 MB10 GB orthomosaic tiling30,000 MB030,720 MBPeak scratch, not input size — a 700 MB compressed granule expands to 1.8–2.5 GB decompressed before the VRT, block cache and outputdraft are counted.
Only the orthomosaic crosses the 10,240 MB ceiling — and no allocation fixes it. That job has to be sharded before it ever reaches a function.

The number that matters in every row of that table is the decompressed size, and it is not derivable from the file listing. A GeoTIFF’s on-disk size is a function of its codec and its content: LZW on a 12-bit Sentinel-2 band typically lands between 2.5:1 and 3.5:1, DEFLATE with a horizontal predictor can reach 4:1 on smooth elevation data, and JPEG-compressed RGB overviews routinely exceed 10:1. Two files of identical byte size can therefore differ by a factor of four once GDAL has them open. The safe rule when sizing EphemeralStorage.Size is to compute from pixel geometry rather than from bytes on disk — width × height × bands × bytes_per_sample — and treat the compressed size only as a lower bound on what the download itself will occupy.

The second structural fact is that /tmp is per execution environment, not per function. Lambda mounts a fresh, isolated NVMe-backed volume into each sandbox, so ten concurrent executions of the same function hold ten independent copies of the allocation and cannot see one another’s files. That is good for isolation and bad for intuition: raising EphemeralStorage.Size from 512 MB to 5,120 MB does not cost ten times more when one invocation runs, but it does when a tiling fan-out drives a hundred of them at once, because the storage charge accrues per environment for as long as each is billed. It also means you cannot use /tmp as a cache shared across a fan-out — anything that must be visible to sibling invocations belongs in S3, ElastiCache, or an EFS mount. It is also the only writable path in the sandbox — /var/task, /opt, and /var/runtime are mounted read-only, so any library that writes beside its own installation fails with Errno 30 until it is redirected.

Beyond raw capacity, the cold start sequence for a GDAL-heavy function already contends for /tmp during shared-library extraction. Insufficient space here aborts the function before a single pixel is processed.

Platform-by-Platform Limits Table

Constraint AWS Lambda GCP Cloud Functions (2nd gen) Azure Functions (Consumption)
Ephemeral storage default 512 MB 512 MB (in-memory tmpfs) ~500 MB (varies by SKU)
Ephemeral storage ceiling 10,240 MB Not configurable (capped at instance RAM) 10 GB (Premium/Dedicated plan only)
Billed separately from memory Yes — $0.0000000367/GB-s above 512 MB No — storage consumes instance RAM No — tied to plan tier
Config knob EphemeralStorage.Size (SAM/CDK/Terraform/console) Instance size (vCPU/RAM tier selection) Storage account binding on Premium plan
Timeout ceiling 15 minutes 60 minutes 10 minutes (Consumption), 60 min (Premium)
Memory ceiling 10,240 MB 32,768 MB 1,536 MB (Consumption)
GDAL temp dir env var CPL_TMPDIR=/tmp/gdal CPL_TMPDIR=/tmp/gdal CPL_TMPDIR=/tmp/gdal or D:\tmp\gdal

GCP Cloud Functions stores temp data in a tmpfs that draws from instance RAM — allocating 4 GB of RAM gives you at most 2 GB of usable /tmp before your function begins swapping. For large raster jobs on GCP, Cloud Run with a mounted Cloud Filestore volume is the more natural fit than Cloud Functions.

Azure Functions on the Consumption plan shares ephemeral storage across function instances on the same host; large geospatial jobs must use a Premium or Dedicated plan with managed disks.

Ephemeral storage configuration and billing across AWS Lambda, GCP Cloud Functions and Azure FunctionsComparison grid of the ephemeral storage quota, the configuration knob that changes it, whether it is billed apart from memory, the memory ceiling and the maximum timeout, for AWS Lambda, GCP Cloud Functions 2nd generation, and Azure Functions on the Consumption plan.Which dial actually changes scratch, per platformAWS LambdaGCP Functions (2nd gen)Azure (Consumption)Ephemeral /tmp10,240 MB512 MB default~8 GBtmpfs, shares RAM~1.5 GBshared with the hostConfig knobEphemeralStorage.SizeSAM, CDK, Terraform, consoleMemory tierno separate dial existsPlan tierPremium or Dedicated onlyBilled apart from memory?Yes$0.0000000367 per GB-sabove 512 MBNostorage is memoryNotied to the planMemory ceiling10,240 MB32,768 MB1,536 MBMax timeout15 min60 min10 minA 250 MB unzipped deployment package sits on top of all of this on Lambda, and the layer extraction that unpacks it competes for the same /tmpduring a cold start.
Lambda is the only one of the three with a scratch dial you can turn independently of memory — which is also why it is the only one where the two axes have to be right-sized separately.

Read those rows against the surrounding quotas, because scratch is never the only ceiling in play. Lambda gives 15 minutes of runtime, 10,240 MB of memory, 10,240 MB of /tmp above a 512 MB default, and 250 MB of unzipped deployment package; GCP Cloud Functions 2nd gen gives 60 minutes and 32,768 MB of memory, out of which roughly 8 GB of tmpfs is carved; Azure Functions on the Consumption plan gives 10 minutes, 1,536 MB of memory, and about 1.5 GB of shared local storage. A workload that fits Lambda’s disk but not its clock has the same outcome as one that fits the clock but not the disk — a failed invocation — so profile both axes against the same representative payload rather than tuning them in sequence.

The asymmetry worth internalising is that only AWS separates the two axes. On Lambda a 1,024 MB-memory function may write 10,240 MB to /tmp quite happily, because the volume is not carved out of RAM; on Cloud Functions the identical code needs a 12 GB memory tier to hold the same files. That single difference usually decides which provider a scratch-heavy raster pipeline belongs on.

Step-by-Step Implementation

1. Baseline your peak scratch requirement

Run this profiler locally against your largest expected input before touching IaC:

python
import shutil
import os
import rasterio
import tempfile

def profile_tmp_usage(input_path: str) -> dict:
    """
    Measure /tmp consumption during a typical rasterio read-transform cycle.
    Run locally with /tmp mounted; translate results to Lambda /tmp sizing.
    """
    os.environ["CPL_TMPDIR"] = "/tmp/gdal_profile"
    os.environ["GDAL_CACHEMAX"] = "512"       # MB — mirror your Lambda setting
    os.environ["GDAL_DATA"] = "/usr/share/gdal"
    os.environ["PROJ_LIB"] = "/usr/share/proj"
    os.makedirs("/tmp/gdal_profile", exist_ok=True)

    before = shutil.disk_usage("/tmp").used

    with rasterio.open(input_path) as src:
        with tempfile.NamedTemporaryFile(suffix=".tif", dir="/tmp", delete=False) as tmp:
            profile = src.profile.copy()
            profile.update(compress="lzw", tiled=True, blockxsize=512, blockysize=512)
            with rasterio.open(tmp.name, "w", **profile) as dst:
                for ji, window in src.block_windows(1):
                    dst.write(src.read(window=window), window=window)
            peak = shutil.disk_usage("/tmp").used
            os.unlink(tmp.name)

    return {
        "before_bytes": before,
        "peak_bytes": peak,
        "delta_mb": (peak - before) / 1_048_576,
    }

Add 20 % headroom to delta_mb and round up to the nearest 512 MB when setting EphemeralStorage.Size.

Composition of a 5,120 MB ephemeral storage allocation for a Sentinel-2 reprojectA 5,120 MB slash tmp allocation broken into five layers: a 2,300 MB decompressed working copy, a 900 MB staged output draft, a 700 MB compressed source copy, 512 MB of GDAL block-cache spill and VRT intermediates, and 708 MB of headroom.Where 5,120 MB of /tmp goes on one 13-band granuleDecompressed working copy13 bands at 10 m, LZW expanded2,300 MBStaged output drafttiled and compressed before upload900 MBCompressed source copyonly if you download instead of streaming700 MBBlock-cache spill and VRTbounded by GDAL_CACHEMAX=512512 MBHeadroom20% over the profiled peak, rounded to 512 MB708 MBProfiled peak 4,412 MB, provisioned 5,120 MB, ceiling 10,240 MB. The 512 MB default would not hold the block cache alone.
Four of these five layers are files your code never names. Sizing from the source file alone under-provisions by roughly a factor of six.

2. Set EphemeralStorage in your IaC

AWS SAM (template.yaml):

yaml
Resources:
  GeoTransformFunction:
    Type: AWS::Serverless::Function
    Properties:
      Runtime: python3.12
      Handler: handler.process
      MemorySize: 3008          # memory controls CPU share and network bandwidth
      EphemeralStorage:
        Size: 5120              # MB — sized to 1.5x peak scratch from profiler
      Timeout: 600
      Environment:
        Variables:
          CPL_TMPDIR: /tmp/gdal
          GDAL_CACHEMAX: "512"
          GDAL_DISABLE_READDIR_ON_OPEN: EMPTY_DIR
          TMPDIR: /tmp
          GDAL_DATA: /opt/share/gdal
          PROJ_LIB: /opt/share/proj

Terraform (aws_lambda_function):

hcl
resource "aws_lambda_function" "spatial_processor" {
  function_name = "spatial-processor"
  runtime       = "python3.12"
  handler       = "handler.process"
  memory_size   = 3008
  timeout       = 600

  ephemeral_storage {
    size = 5120   # MB
  }

  environment {
    variables = {
      CPL_TMPDIR                    = "/tmp/gdal"
      GDAL_CACHEMAX                 = "512"
      GDAL_DISABLE_READDIR_ON_OPEN  = "EMPTY_DIR"
      TMPDIR                        = "/tmp"
      GDAL_DATA                     = "/opt/share/gdal"
      PROJ_LIB                      = "/opt/share/proj"
    }
  }
}

Note: MemorySize and EphemeralStorage.Size are billed independently. Setting both to 10,240 MB doubles your per-invocation cost — right-size each axis separately.

3. Initialise scratch directories at handler entry

The volume outlives the invocation that wrote to it. AWS keeps a warm execution environment alive for several minutes after a request — sometimes far longer under steady traffic — and reuses it wholesale, /tmp included. Nothing between one invocation returning and the next one starting empties the directory; only a sandbox reclaim does, and that is entirely at the platform’s discretion.

Lifecycle of the Lambda ephemeral storage volume across invocationsA four-state lifecycle: the sandbox is created with an empty NVMe slash tmp volume, an invocation writes GDAL intermediates under CPL_TMPDIR, the output is uploaded and the scratch unlinked, and the environment goes warm idle with slash tmp contents intact. The next invocation re-enters the writing state on whatever the previous one left behind./tmp is shared state until the sandbox is reclaimedSandbox createdNVMe /tmp mounted, emptyInvocation writingintermediates underCPL_TMPDIRUpload and unlinkoutput to S3, scratch removedWarm idle/tmp contents persisthandler entryjob completesenvironment retainednext invocation inherits whatever the last one leftEach concurrent execution gets its own volume, so ten parallel invocations hold ten separate copies of the allocation — the quota is perenvironment, not per function.
Nothing between the second and fourth states empties the volume. The dashed return edge is why a stale-file purge belongs on the first line of the handler, not the last.

The practical consequence is that the handler must treat /tmp as inherited state rather than as a clean slate. Purge at entry rather than at exit: an invocation that times out, is throttled mid-flight, or dies to an unhandled exception never reaches its cleanup block, and the files it left behind become the next invocation’s opening balance. A purge at entry runs unconditionally, which is exactly the property you want from the code that guarantees your headroom.

python
import os
import shutil
import tempfile
import logging

logger = logging.getLogger(__name__)

# Module-level initialisation (runs once per cold start)
_TMP_ROOT = "/tmp"
_GDAL_CACHE = "/tmp/gdal"
os.makedirs(_GDAL_CACHE, exist_ok=True)
tempfile.tempdir = _TMP_ROOT

# Explicitly export every GDAL/PROJ path — never rely on defaults
os.environ.setdefault("CPL_TMPDIR", _GDAL_CACHE)
os.environ.setdefault("GDAL_CACHEMAX", "512")
os.environ.setdefault("GDAL_DISABLE_READDIR_ON_OPEN", "EMPTY_DIR")
os.environ.setdefault("TMPDIR", _TMP_ROOT)


def _check_warm_start_stale_files() -> None:
    """Remove any files left by a previous warm-start invocation."""
    for entry in os.scandir(_GDAL_CACHE):
        try:
            os.unlink(entry.path) if entry.is_file() else shutil.rmtree(entry.path)
        except OSError as exc:
            logger.warning("Could not remove stale /tmp file %s: %s", entry.path, exc)


def handler(event: dict, context) -> dict:
    _check_warm_start_stale_files()

    # Emit /tmp baseline usage at invocation start
    usage_start = shutil.disk_usage(_TMP_ROOT).used
    logger.info("tmp_usage_start_mb=%.1f", usage_start / 1_048_576)

    try:
        result = _process(event)
    finally:
        usage_end = shutil.disk_usage(_TMP_ROOT).used
        logger.info("tmp_usage_end_mb=%.1f", usage_end / 1_048_576)

    return result

4. Use block-windowed I/O to cap peak usage

Processing an entire GeoTIFF in memory at once is the single most common cause of /tmp exhaustion. The block_windows() pattern keeps the in-memory footprint bounded regardless of dataset size:

python
import rasterio
import numpy as np
import os

def reproject_raster(src_uri: str, dst_path: str, target_crs: str = "EPSG:3857") -> None:
    """
    Reproject a raster block-by-block, writing directly to /tmp.
    src_uri may be an s3:// path — rasterio streams via VSIFILE.
    """
    from rasterio.warp import calculate_default_transform, reproject, Resampling

    with rasterio.open(src_uri) as src:
        transform, width, height = calculate_default_transform(
            src.crs, target_crs, src.width, src.height, *src.bounds
        )
        profile = src.profile.copy()
        profile.update(
            crs=target_crs,
            transform=transform,
            width=width,
            height=height,
            compress="lzw",
            tiled=True,
            blockxsize=512,
            blockysize=512,
        )

        with rasterio.open(dst_path, "w", **profile) as dst:
            for band_idx in src.indexes:
                reproject(
                    source=rasterio.band(src, band_idx),
                    destination=rasterio.band(dst, band_idx),
                    src_transform=src.transform,
                    src_crs=src.crs,
                    dst_transform=transform,
                    dst_crs=target_crs,
                    resampling=Resampling.lanczos,
                )

For more targeted techniques around GeoTIFF extraction specifically, see Managing /tmp Storage Limits for GeoTIFF Extraction.

Measurement and Verification

Lambda does not emit /tmp utilisation metrics by default. Instrument your functions with CloudWatch custom metrics using the Embedded Metric Format (EMF):

python
import json
import shutil
import time

def emit_tmp_metric(value_mb: float, function_name: str) -> None:
    """
    Write an EMF metric to stdout. CloudWatch Logs ingests this automatically
    and creates a metric in the 'GeoSpatial/Lambda' namespace.
    """
    metric = {
        "_aws": {
            "Timestamp": int(time.time() * 1000),
            "CloudWatchMetrics": [
                {
                    "Namespace": "GeoSpatial/Lambda",
                    "Dimensions": [["FunctionName"]],
                    "Metrics": [{"Name": "TmpUsageMB", "Unit": "Megabytes"}],
                }
            ],
        },
        "FunctionName": function_name,
        "TmpUsageMB": value_mb,
    }
    print(json.dumps(metric))


def snapshot_tmp(label: str, function_name: str) -> float:
    used_mb = shutil.disk_usage("/tmp").used / 1_048_576
    emit_tmp_metric(used_mb, function_name)
    return used_mb

Expected output ranges after tuning:

Metric Healthy range Investigate if
TmpUsageMB at handler entry (warm) < 50 MB > 200 MB (stale files)
Peak TmpUsageMB during processing 40–80 % of EphemeralStorage.Size > 90 %
InitDuration (cold start) < 8 s for a 400 MB container image > 15 s

Use the CloudWatch metric math expression MAX(TmpUsageMB) / EphemeralStorageSizeMB * 100 over a 7-day window to right-size your allocation. Target the 95th-percentile peak, not the average.

Two details make or break this instrumentation. The first is dimension cardinality: EMF creates one metric stream per unique dimension combination, so adding the source S3 key or a job ID as a dimension turns one cheap metric into millions of them. Keep dimensions to FunctionName and at most a small enumerated JobType, and leave the high-cardinality identifiers as plain log properties, still queryable through Logs Insights but not billed as metrics. The second is sampling frequency: a reading taken when the handler returns records the state after GDAL has torn down its intermediates, which is systematically the low point of the invocation. Emit at entry, mid-transform, and exit, or run the background sampler from step 1 and emit only its maximum.

The Cold Start Mapping for Python GDAL page explains how to correlate InitDuration spikes with /tmp write contention during shared-library extraction.

Failure Modes and Debugging

1. DiskSpaceExhausted mid-transform

Signature: OSError: [Errno 28] No space left on device or the Lambda runtime’s DiskSpaceExhausted error, usually mid-write inside rasterio.open(..., "w") or during GDAL’s internal block flush.

Root cause: EphemeralStorage.Size is smaller than the decompressed working set plus GDAL intermediates.

Fix: Increase EphemeralStorage.Size by 2× and re-run. Also set GDAL_CACHEMAX explicitly — an unset value defaults to 5 % of system RAM, which on a 3 GB Lambda is ~150 MB in memory but can generate large disk spills.

2. Silent cache overflow fills /tmp without error

Signature: TmpUsageMB grows monotonically across invocations (visible in your EMF metric), eventually triggering DiskSpaceExhausted on a warm invocation long after the offending file was written.

Root cause: CPL_TMPDIR is unset, so GDAL writes cache files to /tmp outside your managed directories. Without explicit cleanup, warm-start reuse accumulates them.

Fix:

python
# Always set before importing gdal or rasterio
import os
os.environ["CPL_TMPDIR"] = "/tmp/gdal"
os.makedirs("/tmp/gdal", exist_ok=True)

Then add a glob.glob("/tmp/gdal/*") purge at handler entry.

3. OSError: [Errno 30] Read-only file system

Signature: Immediate crash on the first file write, usually inside a library that defaults to /var/task or /var/runtime.

Root cause: Everything outside /tmp in Lambda is read-only. Some GDAL plugins or Python packages write to tempfile.gettempdir() which can resolve to /var if TMPDIR is not set.

Fix: Export TMPDIR=/tmp in your Lambda environment variables and confirm tempfile.gettempdir() returns /tmp from a test invocation.

4. Warm-start stale file collision

Signature: Transient FileExistsError or corrupted output on a fraction of invocations, correlating with high concurrency.

Root cause: A previous warm-start invocation left a partial output file at the same deterministic path (e.g., /tmp/output.tif). The next invocation tries to create it anew.

Fix: Use tempfile.NamedTemporaryFile(dir="/tmp", suffix=".tif", delete=False) to generate unique paths, or add a UUID prefix: f"/tmp/{uuid.uuid4().hex}_output.tif".

5. GeoPackage / SQLite journal exhausts /tmp

Signature: sqlite3.OperationalError: disk I/O error during fiona or GDAL GeoPackage writes.

Root cause: SQLite’s WAL journal can double the effective file size during large transactions. A 1 GB GeoPackage write briefly needs 2 GB of /tmp.

Fix: Set OGR_GPKG_JOURNAL_MODE=MEMORY as a GDAL config option, or reduce transaction batch size so the WAL journal stays small.

6. Concurrent invocations exhaust the account’s storage budget, not the function’s

Signature: Individual invocations stay well inside their allocation, yet the cost of Lambda-Storage-Duration climbs faster than invocation count during tiling fan-outs.

Root cause: Each concurrent execution environment holds its own EphemeralStorage.Size allocation for as long as it is billed. A function configured at 10,240 MB running at 200 concurrent executions is billing roughly 2 TB of storage-seconds while the burst lasts, even though no single invocation used more than 3 GB.

Fix: Size storage for the per-invocation peak, not for the worst input you can imagine, and reserve concurrency on the tile processor so a runaway fan-out cannot multiply the allocation without bound. If one rare oversized input drives the ceiling, route that input to a separate function with its own larger allocation instead of raising the setting for every tile.

Cost and Scaling Considerations

Per-invocation cost math

Storage above 512 MB is billed at $0.0000000367 per GB-second (us-east-1, June 2026).

For a 5,120 MB allocation running a 60-second GeoTIFF reproject:

code
Billable storage = (5,120 − 512) MB = 4,608 MB = 4.5 GB
Cost = 4.5 GB × 60 s × $0.0000000367 = ~$0.0000099 per invocation

At 1 million invocations per month: ~$9.90/month in storage alone. Compare this with memory billing (which also covers CPU) before optimising the wrong axis.

That comparison is usually decisive. Memory GB-seconds bill at roughly $0.0000166667 in us-east-1 — about 450 times the storage rate — and memory is what buys you CPU, so the same 4.5 GB held as RAM for the same 60 seconds costs on the order of $0.0045 per invocation against $0.0000099 for storage. The engineering conclusion follows directly: when the constraint is scratch space rather than throughput, buy storage, and when a job is slow rather than cramped, buy memory. Setting both axes to 10,240 MB “to be safe” is the most common way to double a Lambda bill without moving either the failure rate or the p95 duration.

Duration is the multiplier that catches people out. Storage bills for the whole billed duration, not for the window in which files exist, so a function that writes intermediates for 8 seconds and then uploads for 50 pays for its full 5,120 MB across all 58. Streaming the upload while the transform is still running removes storage-seconds that were buying nothing.

When to prefer alternatives

Scenario Better than expanding /tmp
Input > 8 GB per job AWS Fargate with EFS or FSx for Lustre
Jobs run serially in a single region S3 Object Lambda with streaming transform
Output must land in a database AWS Step Functions with per-tile Lambda + RDS
Temp data must survive > 15 minutes EFS mounted to Lambda (no size limit)
High-concurrency tile generation Lambda + /tmp per tile (keep tiles small, < 50 MB each)

EFS mounts eliminate the /tmp ceiling entirely but add ~10 ms of cold-start latency per mount point. For workloads where 10 ms matters, profile against the memory and CPU allocation patterns before adding EFS.

At scale, the EphemeralStorage.Size setting becomes a cost-optimisation variable: use Lambda Power Tuning (AWS open-source tool) to sweep storage sizes alongside memory across a representative test payload and find the cheapest configuration that avoids DiskSpaceExhausted.

The IAM Security Boundaries for Cloud GIS page covers the complementary topic of scoping S3 write permissions to exactly the output prefix your Lambda targets — important when /tmp intermediates get uploaded during error recovery.

Frequently Asked Questions

What is the maximum /tmp storage in AWS Lambda?

512 MB to 10,240 MB (10 GB). The default is 512 MB if you do not set EphemeralStorage.Size. You can increase it in the console, SAM, CDK, or Terraform independently of memory allocation.

Does /tmp persist between Lambda invocations?

Yes, during warm starts the execution environment is reused and /tmp contents survive. Data is only wiped when AWS reclaims the sandbox. Always clean up or check for stale files at the start of each handler.

How much /tmp does a 2 GB GeoTIFF need?

Plan for 3–4 GB: the compressed source (~2 GB), an uncompressed working copy (~2–3× compressed size depending on band count), plus GDAL VRT and block-cache intermediates. A 4 GB /tmp allocation with GDAL_CACHEMAX capped at 512 MB is a safe starting point.

Is ephemeral storage billed separately from memory?

Yes. Storage above 512 MB is billed at $0.0000000367 per GB-second. For a 4 GB allocation running 30 seconds, that is roughly (4 − 0.5) × 30 × $0.0000000367 ≈ $0.0000039 per invocation — negligible per call but material at millions of invocations per month.

Do concurrent invocations share the same /tmp?

No. Every execution environment receives its own isolated volume, so a function running at 50 concurrent executions holds 50 separate copies of whatever you configured. Two invocations of the same function cannot see each other’s files, which rules out using /tmp as a shared cache across a tile fan-out — that state belongs in S3, DynamoDB, or an EFS mount. It also means the storage charge scales with concurrency, not with invocation count.

When should I use EFS instead of /tmp?

When the working set genuinely exceeds 10,240 MB, when intermediate data must outlive a single 15-minute invocation, or when several concurrent invocations need to read the same staged files. EFS removes the size ceiling entirely and is shared across executions, at the cost of roughly 10 ms of extra cold-start latency per mount point, VPC configuration, and per-GB storage charges that continue whether the function runs or not. For anything that fits in 10 GB and finishes inside one invocation, /tmp is faster and cheaper.


Guides in this topic

Back to Serverless Geospatial Architecture & Platform Limits