Monthly Median Composites, One Machine per Month
Cloud-free monthly composites are usually an overnight batch job. This canvas runs every month in your date range on its own machine, all at once, and fills in a month grid as each one finishes. In the run shown below, 37 monthly composites finished in 6.2 seconds of wall time from 61.5 seconds of total work, spread across 37 machines.
Try it out
A 12-month range over a few square kilometres returns in a couple of seconds. Widening to five years barely changes the wall time, since each extra month gets its own machine.
1 · Request
The widget is a MapLibre map with a Draw box tool. If you don't draw a box, the pipeline uses the current map view. The other inputs are:
- Start and end date
- Output:
NDVI medianorRGB median - Max cloud cover (%)
- Scenes / month:
0composites every scene in the month; a small number samples that many evenly across it.
Run pipeline sends these as one JSON pipeline_request parameter to the three nodes below, with a run_id timestamp so each run is distinct from the last.
2 · Scene search
This UDF queries Fused's partitioned Sentinel-2 L2A STAC index (s3://fused-asset/stac/sentinel-2-l2a/partitioned_2015_2025/) for scenes that intersect your AOI, filters by date range and cloud cover, and drops duplicate products.
It builds COG URLs directly from each product URI (mission, date and MGRS tile map to a sentinel-cogs.s3.us-west-2.amazonaws.com path) instead of resolving STAC assets. The output is a (scene_id, datetime, band, url, cloud_cover) table with Red, NIR and SCL for NDVI, plus Green and Blue for RGB.
3 · Monthly fan-out
This node plans the run from the request alone. It lists the YYYY-MM months in the date range, sets the output size (10 m/px, at most 640 px on the long edge), and hashes the request into a 16-character key. Results are stored under fd://monthly_median/<key>, so the same request always maps to the same location.
Scenes are grouped by month, and each month becomes one call to .map():
# doctest: skip
result = month_udf.map(
args, # one dict per calendar month
engine="remote",
max_workers=len(args), # one machine per month, all at once
worker_concurrency=1,
cache_max_age="30d",
max_retry=1,
).df()
Setting max_workers=len(args) with worker_concurrency=1 gives each month its own machine, so 37 months run on 37 machines in parallel. Each machine:
- Reads the month's scenes with windowed rasterio reads on a 12-thread pool.
- Masks clouds using the Sentinel-2 SCL band.
- Computes NDVI as
(NIR − Red) / (NIR + Red). - Takes a
nanmedianacross the scenes, so a pixel clouded in some passes still gets a value from the others.
It then writes three objects to storage: a colorized PNG for display, a grayscale PNG with the quantized NDVI values, and a JSON status file (scene count, valid-pixel coverage, run time, worker). Nothing is returned through the pipeline; the view reads results straight from storage.
4 · Month grid
The output node recomputes the same plan, so it knows every month's storage path before any composite exists. It signs a URL for each month and returns a page that polls storage once a second, replacing each "queued" card with its image as soon as that month finishes.
Click a month to enlarge it, and hover over the image to read the median NDVI at that pixel. When the last month lands, the footer shows wall time against total worker time.
This node uses cache_max_age="0s" on purpose. The signed URLs expire an hour after signing, so a cached page would eventually show broken images.
About the data
| Property | Value |
|---|---|
| Imagery | Sentinel-2 L2A COGs, AWS Open Data (anonymous) |
| Scene catalog | Fused STAC index, partitioned 2015–2025 |
| Bands | Red (B04), NIR (B08), SCL, plus Green (B03) and Blue (B02) in RGB mode |
| Cloud masking | SCL classes 0, 1, 3, 7–11 |
| NDVI display range | −0.2 (built-up / bare soil) to 0.8 (dense vegetation) |
See also
- Sentinel-2 Composite: a single composite of any three bands over the current map view, served as tiles.
- NDVI Time Series for Farm Fields: the same Sentinel-2 index sampled per field as time series instead of monthly rasters.
- AI Change Detection: year-over-year change from embeddings instead of a spectral index.
- Parallel execution with fused.submit: the fan-out primitive behind
.map().