
Routing Glacial Floods for $0.00076 Per Run Using AWS Step Functions and Lambda
How I built CASCADE-GLOF, a serverless telemetry engine on AWS that predicts glacial lake outburst floods using Sentinel-2 satellite data and Muskingum-Cunge routing for under a tenth of a cent per execution.
1. The 2023 Sikkim flood
On the night of 3 October 2023, the moraine dam holding South Lhonak Lake in north Sikkim failed. Seismic records place the collapse at 22:12 IST. About 50 million m³ of water drained into the Teesta valley. Roughly 2 hours 18 minutes later it reached the 1,200 MW Teesta-III dam at Chungthang, 67.5 km downstream, and destroyed it.
Before it is a modelling problem, a glacial lake outburst flood (GLOF) is a screening problem. An operator below a glacial lake needs two numbers in advance: how large the flood could be, and how many minutes of warning the valley gets. CASCADE produces those numbers from public satellite data, and I validated it by replaying the 2023 event.
The workload has three distinct phases:
- Lake measurement: embarrassingly parallel, one job per lake-month, I/O-bound reads of satellite imagery.
- Flood routing: CPU-bound numerics per lake.
- Publishing: a small aggregation step.
2. The serverless architecture
Everything is one AWS SAM template: 30 resources in us-west-2, deliberately co-located with the Sentinel-2 L2A Cloud-Optimized GeoTIFFs on the Registry of Open Data on AWS.
Orchestration. A Standard AWS Step Functions workflow runs
PlanJobs → MeasureMonths → RouteFloods → PublishDossier → Notify .-
MeasureMonthsis an inline Map over one item per lake-month, capped at 8 concurrent iterations.
-
RouteFloodsmaps over lakes.
JSON
"MeasureMonths": {"Type": "Map","ItemsPath": "$.plan.items","MaxConcurrency": 8,"ItemProcessor": { "ProcessorConfig": { "Mode": "INLINE" }, "StartAt": "MeasureLakeMonth", "States": { ... } },"ResultPath": "$.observations"}
Compute. A single container image in Amazon ECR serves four Lambda functions.
- The image: Python 3.14 with rasterio/GDAL, pyproj and shapely; 1.18 GB uncompressed.
- The functions differ only in
ImageConfig.Command, memory and timeout.measuregets 3,008 MB and 900 s;routegets 2,048 MB and 300 s;planandreportget 512 MB.
Data access. GDAL runs with
AWS_NO_SIGN_REQUEST=YES and GDAL_DISABLE_READDIR_ON_OPEN=EMPTY_DIR . Each job issues HTTP range reads for only the window around the lake's 10 m UTM grid; no scene is ever downloaded. The Copernicus DEM is read once per lake, and the traced flow path and cross-sections are cached in Amazon S3.State and security.
- Storage: observations go to on-demand Amazon DynamoDB. Results go to a private, TLS-only S3 bucket.
- IAM: each function has its own role, scoped to specific S3 prefixes and
dynamodb:PutItem.
Failure semantics.
- Retries: tasks retry transient Lambda errors with exponential backoff and full jitter.
measurealso retriesStates.TaskFailedonce after 30 s.
- Catches: a Catch records a failed month, and the run continues.
- Alerts: the alert step uses the native
sns:publishintegration withResultPath: null, so execution output is unchanged. A Choice state guards it, because a missing JSONPath inParametersraisesStates.Runtime, which no Catch can handle.
3. Overcoming friction
Cold starts on a 1.18 GB image.
- The first run: the first execution after the initial deploy took 81.2 s. Four of five invocations logged
INIT_REPORT … Status: timeoutat the 10-second init limit. Lambda then re-ran initialisation inside the invoke phase, so nothing failed, but the latency was real.
- After that: once the image was cached, cold starts fell to 1.0–3.3 s.
- A related fix: the Python 3.14 base image lacks
libexpat, which rasterio's bundled GDAL needs, sodnf install expatwent into the Dockerfile.
The mitigations are operational:
- run a warm-up execution after any image change;
- keep infrastructure-only deploys from touching the image.
sam buildreproduced the identical digest, and every reviewed change set showed zero function rows.
The next step is lazy-loading the geospatial stack inside the handlers, to keep init under 10 s.
Lambda concurrency. The new account's Lambda concurrency limit was 10, so reserved concurrency was impossible. The Map's
MaxConcurrency of 8 keeps the fan-out under it.The CloudFront rollout that never was. The plan was a private S3 bucket behind CloudFront with origin access control.
- The change set looked correct: four additions and one bucket-policy modification.
- At execution:
CREATE_FAILEDwith "Your account must be verified before you can add new CloudFront resources."
- The rollback was clean (
UPDATE_ROLLBACK_COMPLETE). The only debris was a site bucket with aRetainpolicy, which I deleted by hand.
I pivoted the same day. A small zip-packaged Lambda behind a function URL now serves the page.
- What it serves: the site bucket, plus three published prefixes from the results bucket.
- Hardening: GET and HEAD only, normalised paths, and security headers set in code.
- What it didn't touch: the container image.
The lesson: a change set validates your template, not your account's eligibility. On new accounts, check service-level verification before designing around a service.
4. Cost and performance
I benchmarked 60 back-to-back executions after two manual runs. All 62 succeeded.
Metric | Value |
|---|---|
Warm end-to-end median | 7.1 s (95% CI 7.0–7.2 s) |
Warm p95 / max | 8.0 s / 8.2 s |
measure warm median (per lake-month) | 5.55 s |
route / report warm median | 0.473 s / 0.179 s |
Lambda usage (60 runs) | 2,094 GB-s, 300 requests |
Cost per execution (on-demand) | $0.00076 |
That is $0.0455 for the whole batch, inside the free tier.
measure peaks at 405 MB of its 3,008 MB; the extra memory buys CPU share, and right-sizing it is next. Warm runs re-read identical windows that GDAL can cache, so first reads of new scenes should run closer to 10–12 s per lake-month.For auditability, the raw execution histories, Lambda
REPORT lines, CloudTrail StartExecution records and metrics are published (redacted) in the repository. CI rebuilds the benchmark tables from them on every push. One finding there: CloudWatch's ExecutionsSucceeded reported 61 for 62 successful executions. For evidence, trust execution records over aggregate metrics.Results and limits
Against the 2023 event:
- Lake area: within 1.8% of ISRO's figure.
- Flow path: within 1.9% of the published 67.5 km.
- Forecast replay: a pre-registered replay using only the 14 September measurement forecast an arrival window of 101–183 minutes. The real flood took 138.
- Limits: arrival timing is calibrated, and the modelled peak runs 1.4–2× above published reconstructions. Treat it as screening, not design.
Code and evidence: github.com/suryaprasanthcse/CASCADE-GLOF-Telemetry · Demo: YouTube · Live dossier: CASCADE page
Built solo during WeMakeDevs × AWS Environmental Hacks, with Claude Code as the coding agent. Contains modified Copernicus Sentinel data [2023].
Enjoyed reading this content? Let the author know!
Your likes, comments, shares, and saves help creators reach more builders.
Loading recommendations
Loading article