Documentation

Point tileserver-rs at any SpatioTemporal Asset Catalog (STAC) API and serve the underlying Cloud-Optimized GeoTIFFs (COGs) as raster tiles — no ingest, no pre-processing, no local copy.

Three modes

ModeTriggerUse when
Static discoverydynamic = false (default)One-shot visualisation of the most recent asset. Ingested once on startup.
Dynamic per-tile discoverydynamic = true, max_items = 1Always-fresh per-tile lookups; tiles come from whichever single asset intersects.
Multi-asset mosaicdynamic = true, max_items >= 2Composite several overlapping items (cloud-free mosaic, time-stack, multi-sensor).

Enable the feature at build time:

cargo build --release --features stac

Static discovery

Point at a STAC API collection; tileserver-rs performs a single /search on startup, merges the discovered bounding boxes, and serves the first COG asset.

[[sources]]
id = "sentinel2"
type = "stac"
path = "https://earth-search.aws.element84.com/v1"
collection = "sentinel-2-l2a"
asset_role = "visual"        # or "thumbnail" / a specific asset key
dynamic = false
max_items = 25               # cap the catalog scan

Served at GET /data/sentinel2/{z}/{x}/{y}.png.

Dynamic per-tile discovery

Flip dynamic = true and every incoming tile triggers a fresh STAC /search with the tile's WGS-84 bounding box:

[[sources]]
id = "sentinel2-dynamic"
type = "stac"
path = "https://earth-search.aws.element84.com/v1"
collection = "sentinel-2-l2a"
asset_role = "visual"
dynamic = true
max_items = 1                # single best asset per tile

Tiles always reflect the current catalog state — useful for dashboards that must show the latest imagery without restarts.

Cost: one STAC API call + one COG range-read per unique tile. Pair with a global cache (see [cache] config) to amortise hot-path cost.

Multi-asset mosaic compositing

Bump max_items to fetch multiple candidate items and composite them in-memory:

[[sources]]
id = "sentinel2-mosaic"
type = "stac"
path = "https://earth-search.aws.element84.com/v1"
collection = "sentinel-2-l2a"
asset_role = "visual"
dynamic = true
max_items = 5                # top 5 candidates, composited
pixel_selection = "first"    # default; see below

Semantics:

  • STAC /search is asked for up to max_items intersecting items; results come back ranked (newest first, least cloud first — however the API sorts).
  • All candidates render concurrently via futures::future::join_all, so cold-cache latency is max(per-asset) instead of sum(per-asset).
  • The configured pixel-selection method reduces the stack of RGBA layers into a single tile.
  • Any single failed COG is logged and skipped — one bad asset does not blank the tile.

The result is a seamless cloud-free-ish mosaic without any preprocessing step.

Pixel selection

pixel_selection chooses how the per-asset renders combine into the final tile. Names match rio-tiler's mosaic methods so configs are interchangeable with titiler.

ValueBehaviourShort-circuits?
first (default)Highest-priority valid pixel wins; lower-priority assets fill in where the canvas is masked.Yes — stops fetching once the tile is fully opaque.
highestPer-pixel maximum across all valid inputs. Useful for snow/water glare.No
lowestPer-pixel minimum. Useful for deep-shadow emphasis.No
meanPer-pixel arithmetic mean over valid inputs.No
medianPer-pixel exact median — more robust to outliers than mean.No
stdevPer-pixel standard deviation — useful for change detection.No
countEncode the per-pixel count of valid contributions (debug/QA).No
lowest_cloud_coverSort candidates by eo:cloud_cover ascending before feeding, then run the first reduction. Clearest-sky asset wins on top.Yes

Assets that lack eo:cloud_cover metadata sort to the end of the priority list and are treated as worst-case (∞ cloud cover), so any explicitly-clear asset always beats an unlabelled one.

Resampling

resampling controls how source COGs are rescaled to the 256×256 Web Mercator output grid. Defaults to bilinear; matters most when a band's native resolution differs from the served tile size (e.g. Sentinel-2 10 m → 256×256 at zoom 13 needs downsampling).

[[sources]]
id = "sentinel2"
type = "stac"
path = "https://earth-search.aws.element84.com/v1"
collection = "sentinel-2-l2a"
resampling = "cubic"          # sharper than bilinear for continuous imagery
ValueSource pixelsWhen to use
nearest1Classified/categorical rasters, colourmap-indexed bands.
bilinear (default)2×2Continuous imagery; balanced quality/speed.
cubic4×4Sharper downsampling, moderate CPU cost.
cubicspline4×4 smoothedSofter edges; good for elevation.
lanczos6×6 windowed sincBest quality, highest CPU.
averageblockHeavy downsampling; anti-aliases.
modemost-frequentCategorical data at low zooms.

Sentinel-2 cross-resolution mosaics (10 m RGB + 20 m SWIR) go through the same resampler; GDAL handles the per-band rescaling automatically.

Band math

Append ?expression=<formula> to any raster tile URL to evaluate a band-math expression pixel-by-pixel before the tile is encoded. Band references use b1, b2, ... (1-based) and map to the source's native band order.

GET /data/sentinel2/{z}/{x}/{y}.png?expression=(b2-b1)/(b2+b1)

The built-in parser supports arithmetic (+ - * / ^), common functions (sin, cos, log, exp, sqrt, etc.) and parentheses — anything rio-tiler / titiler would accept via numexpr. Missing or out-of-range band references (e.g. b99 on a 3-band raster) return the original tile unchanged with a WARN-level log, so a broken expression never blanks the map.

Example indices

NameExpressionNotes
NDVI(b2-b1)/(b2+b1)red=b1, NIR=b2
NDWI(b1-b2)/(b1+b2)green=b1, NIR=b2
SAVI (L=0.5)((b2-b1)/(b2+b1+0.5))*1.5soil-adjusted
Simple ratiob2/b1quick contrast boost

Output is always a single-band greyscale PNG; pair with a colourmap-indexed MapLibre raster layer if you want false-colour visualisation.

Element84 Earth Search quick example

A fully working config for global Sentinel-2 L2A imagery via Element84's public API:

[[sources]]
id = "sentinel2"
type = "stac"
path = "https://earth-search.aws.element84.com/v1"
collection = "sentinel-2-l2a"
asset_role = "visual"
dynamic = true
max_items = 5

[[styles]]
id = "sentinel2"
path = "./data/styles/sentinel2/style.json"
name = "Sentinel-2 L2A"

Style JSON references the raster source:

{
  "version": 8,
  "sources": {
    "sentinel2": { "type": "raster", "url": "/data/sentinel2.json", "tileSize": 256 }
  },
  "layers": [
    { "id": "bg", "type": "background", "paint": { "background-color": "#000" } },
    { "id": "sentinel2", "type": "raster", "source": "sentinel2" }
  ]
}

Supported STAC catalogs

Any STAC API that implements the /search endpoint (STAC API v1.0+) with a POST body accepting collections, bbox, and limit:

  • Element84 Earth Search — https://earth-search.aws.element84.com/v1
  • Microsoft Planetary Computer — https://planetarycomputer.microsoft.com/api/stac/v1
  • USGS Landsat STAC — https://landsatlook.usgs.gov/stac-server
  • NASA CMR STAC — https://cmr.earthdata.nasa.gov/stac/…
  • Self-hosted stac-fastapi / stac-server

Asset selection

tileserver-rs picks one COG asset per item using this priority order:

  1. Asset with a roles array containing the configured asset_role value.
  2. Asset keyed by the configured asset_role string.
  3. Asset whose type is image/tiff; application=geotiff; profile=cloud-optimized or starts with image/tiff.

3D bounding box handling

STAC items may carry 6-element bounding boxes [west, south, min_elev, east, north, max_elev]. tileserver-rs correctly extracts the 2D footprint ([0, 1, 3, 4]) so elevation metadata never displaces tiles.

Specifications

SpecLink
STAC Corestacspec.org
STAC APIgithub.com/radiantearth/stac-api-spec
Cloud-Optimized GeoTIFFcogeo.org