This is the multi-page printable view of this section. .
Guides
- 1: Styling and band math
- 2: Mosaics and STAC
- 3: Production operation
- 4: Runtime limits and source consistency
- 5: Local workload and benchmarks
Task-oriented guides for the capabilities most applications compose.
1 - Styling and band math
Inputs use SDK raster-style = "=0.4.0", protocol 3.0, and Rust 1.92.
The SDK validates structure and semantics; the adapter rejects unsupported
capabilities. Unknown fields and duplicate singleton parameters are errors.
The subset below targets Tiler::tile and MosaicTiler::tile, not full backend certification.
Rust inputs
Use TileStyle::from_raster_query for an original Q3 string, or
TileStyle::from_raster_style(&RasterStyle) for an SDK-validated document.
RasterStyle is re-exported from async_geotiff::style.
style.raster_style_json() returns normalized JSON. Reparse the document to
change a style. The parsed style is immutable; format() and tilesize() expose read-only output settings.
Tile and Mosaic are versioned extensions: list their exact Schema URLs in
extensions, and place settings in raster:tile / raster:mosaic.
Q3 carries the URL array in extensions and the fields object in extension_fields.
Use raster_style::encode_query to encode a validated SDK document.
Incomplete validation and third-party extension declarations are rejected before execution.
HTTP Q3
Single-band pseudocolor:
RGB with one range broadcast to all three outputs:
dataset, mode, and _v belong to the example server, not the style document.
TMS and tile coordinates remain in the path. The SDK decodes raw query fragments
once: encode + as %2B and spaces as %20. Never pre-decode Q3.
JSON is a Rust API entrypoint; the example HTTP service uses Q3 GET requests.
PNG is the default. WebP requires the webp feature, a declared Tile 1.0.0 extension with raster:tile: {format: "webp", lossless: true},
and a .webp path. The path extension must match the style format.
Stretch and color
- Renderers:
gray,rgb,pseudocolor,categorized,single_color. method=none, including omission, uses[0,255]on display paths and never triggers automatic statistics.method=linearrequiresrescale; RGB accepts one broadcast or three channel ranges.- Display paths support
range_policy=clamp|transparent, checked before color operations. - Named ramps use the project’s pinned
colorous256-entry tables: viridis, magma, inferno, plasma, cividis, turbo, greys. - Native
colormapobjects use exact data values; interval arrays use half-open[lo,hi)bounds, including the final interval. With explicit stretch or Gamma, pseudocolor uses 0–255 lookup indices; categorized always uses exact data values. - Gray supports
renderer_invert; valid zero and negative inputs work withsingle_color. opacitymultiplies output Alpha. Quantized zero Alpha produces transparent black.
Categorized supports explicit exact-value tables and nearest only. It reads the base level because source overviews lack category-preservation evidence. Numeric paths support the existing nearest, bilinear, cubic, cubic_spline, lanczos, and average kernels. Dependencies are resampled independently, preserving validity. Source Alpha bands are currently rejected before pixel reads.
cubic_spline smooths even at integer pixel centers. It therefore still runs
its kernel when the requested tile grid exactly matches the source grid;
direct tile rendering and the warp path preserve the same resampling semantics.
Cubic, CubicSpline and Lanczos also preserve finite constant F64 values near
the numeric limits without making them transparent through intermediate
overflow. Truly non-finite samples or unrepresentable results remain invalid.
Statistical stretch and curves
All seven methods are supported: none, linear, minmax, percentile,
stddev, histogram_equalization, and curve. The examples expose their parameters:
Percentiles use linear interpolation of sorted samples; standard deviation is
population deviation. Histogram equalization uses the empirical CDF, without bins.
Curve points are [data value, normalized output]; provide one broadcast curve
or three RGB curves. Percent-encode the JSON in an actual Q3 URL.
Tiler and MosaicTiler bind a fixed 1024 × 1024 EPSG:4326 grid over the
complete source footprint or union of the fixed mosaic collection. The grid has
north-up rows, equally spaced longitude/latitude centers, and coverage/validity
masks inherited from each source. Overview choice is fixed from each source’s full
extent at this grid size, independent of tile zoom. The same upstream resampling,
NoData, expressions, tuple validity and mosaic stages run on that grid. Statistics
never come from the requested tile or unverified embedded GDAL summaries.
Omitted statistics means dataset/mosaic scope, sample accuracy and a maximum of
1,000,000 row-major evenly spaced grid positions. statistics={"scope":"dataset", "accuracy":"exact"} visits all cells of this bound grid, not every native
resolution source pixel. NoData samples are discarded without replacement. Empty
statistics give transparent output; a constant range maps its value to 0.5.
Statistics are shared across tiles and cached with reader instance/order and the full normalized style. CPU work, source reads and retained median inputs use the existing resource limits. Complete reads of the selected overview can be rejected when they exceed the work budget. Two cached plans bound retained CDF storage.
MosaicJSON with quadkey_zoom=0 provides a complete fixed collection for statistics.
Partitioned MosaicJSON statistical stretches fail explicitly: flattening its index
would lose coverage masks and regional paint order. Tile rendering always uses the
index-selected assets. Custom MosaicSource implementations must provide
statistics_assets() whose full-footprint composite preserves tile selection semantics. Unbound dynamic STAC collections, viewport scope and external
statistics.ref snapshots fail explicitly. Low-level render(ImageData, ...)
requires a resolved plan for statistical methods; use the tilers to bind it.
Expressions
Decimal literals, parentheses, unary minus, and arithmetic + - * / are supported.
Band numbers are one-based; RGB expressions separate three outputs with ;.
This NDVI example requires band 5 to be near-infrared and band 4 red; check the
asset metadata first. Expressions cannot coexist with bidx, but may combine
with supported Gamma operations. Invalid inputs, division by zero, and non-finite
results become transparent. Functions, conditionals, and scientific notation
are unsupported. Both the SDK and existing arithmetic parser bound input size.
Color formulas
effects.color_formula supports Gamma, evaluated in array order. Q3 example:
Unsupported requests fail explicitly: external statistics snapshots, calibration other than none, index, color_mapping, source palettes, mode resampling, terrain, third-party extensions, other color operations, JPEG, and lossy WebP.
Mosaic
With the default mosaic feature enabled, declare this JSON member:
Declare the official Mosaic 1.0.0 schema URL in extensions. Q3 encodes the declaration array in extensions and the object containing raster:mosaic in extension_fields. The old mosaic short parameters are rejected.
All five methods (first, highest, lowest, mean, median) support before_channels
and after_channels. Call mosaic.tile(coord, &style).await? to take the
selection from the document. tile_with_options(coord, &style, &options) adds request cancellation and deadlines.
Before channels, ranking positions refer to deduplicated source dependencies
sorted by source band number; after channels, positions refer to output order.
rank_channel is one-based and only applies to highest/lowest. Only complete,
valid tuples from one source participate; channels are never stitched across
sources. Gamma, colormaps, and opacity apply once after merging. The source
Alpha limitation still applies.
Assets remain outside the style, supplied by MosaicJSON, STAC, or a custom MosaicSource. See the mosaic guide for configuration and limits.
Migration
Removed: from_query / from_pairs, ValidatedTileStyle, compile_validated,
mutable style fields, Stretch / ColourDefinition / Colour / lut_from_stops, and the separate mosaic
selection argument. Use from_json / from_raster_query / from_raster_style.
TileStyle::default() is a protocol gray-band-1 style with fixed 0–255 mapping.
HTTP requires version=3.0&type=...; old parameters and unknown fields fail.
All viewers emit Q3 only. Opacity is applied once by the server; MapLibre offers
mosaic stage and rank-channel controls.
| Legacy input | Raster Style input |
|---|---|
bidx=1&colormap_name=viridis&rescale=0,3000 | Add version=3.0&type=pseudocolor&method=linear |
| Three identical RGB ranges | One broadcast range; explicitly set type=rgb and three bidx values |
| Omitted stretch auto-selects stddev for non-U8 | Fixed [0,255]; explicitly supply a range when needed |
Continuous stops in colormap | Do not reuse: native arrays are interval tables; continuous color_mapping is not yet implemented |
stretch=percent&pc=2,98 | method=percentile&percentiles=2,98 |
2 - Mosaics and STAC
The default-enabled mosaic Cargo feature adds MosaicTiler and the
MosaicSource trait; applications that do not need mosaics can disable default
features. Because that also disables proj, use
--no-default-features --features proj when general CRS transformation is
still required. The source maps a tile coordinate to assets; the tiler opens
and warps those assets, reduces overlapping pixels, and renders the composite
once.
MosaicJSON
Run the embedded example or pass a MosaicJSON file:
The parser accepts MosaicJSON 0.0.3. Its quadkey index is defined in
WebMercatorQuad; using a MosaicJSON source with WorldCRS84Quad returns a
query error.
Connect the example server to a file at startup:
Tiles are then available under /mosaic/tiles/....
Raster Style 3.0
before_channels merges dependencies before evaluating expressions;
after_channels evaluates each source first, then merges outputs. The example’s
dependency table is [1,2,3], so rank_channel=1 compares source band 1;
after_channels would compare the first output, source band 3. Ties preserve the
first source’s complete tuple. Mean/median use the same complete valid tuples
for every channel and avoid intermediate overflow for extreme finite values.
Q3 example:
In MapLibre, select mosaic under Source to configure stage and ranking. Asset locations remain server-bound; JSON/Q3 does not contain the asset list. Protocol mosaics require explicit configuration. Source Alpha bands remain unsupported and return a capability error. See the styling guide.
Pixel selection
| Strategy | Behavior |
|---|---|
First | First valid pixel wins; stops early after the tile is fully covered. |
Highest | Select the complete tuple with maximum rank_channel. |
Lowest | Select the complete tuple with minimum rank_channel. |
Mean | Streaming mean of valid samples; constant memory in asset count. |
Median | Median of valid samples; retains every contributing image until completion. |
Both bidx and rank_channel are one-based; the latter is a position at the merge stage.
MosaicConfig defaults to 8 assets processed concurrently per chunk, an
opened-reader capacity of 256 in each MosaicTiler reader pool, and a hard cap
of 64 assets per requested tile. Exceeding the cap returns an error,
never a truncated partial result. Configure all
three limits through the builder:
Keep the asset cap enabled for untrusted or low-zoom indexes to prevent fetch amplification. Choose deterministic source order for first and extremum ties. Median additionally limits retained inputs to 256 MiB and observes the shared work budget.
STAC demo
Enable stac and provide all three startup settings:
The API, collection, and asset key are fixed at process startup. HTTP query parameters cannot turn the demo into an arbitrary STAC or COG proxy.
After startup, open http://127.0.0.1:8080/maplibre. When STAC is configured, the MapLibre viewer selects mosaic mode by default and lets you verify the configured collection without guessing a tile coordinate.
For custom authentication, proxies or timeouts, pass a configured reqwest 0.11
ClientBuilder to StacSource::with_http_client_builder before building it.
The source retains those settings and enforces at most ten same-origin redirect
hops. Cross-origin redirects are rejected before sending credentials, and
transport errors omit full URLs. The deprecated with_http_client now fails
before I/O because an already built client’s redirect policy cannot be replaced.
StacSource::new keeps its existing signature.
HTTP asset policy
The default policy allows public and private HTTP(S) assets but blocks
loopback, link-local, and cloud-metadata destinations. Use
HttpAccessPolicy::strict() for fully untrusted documents: it requires HTTPS
and also blocks private address ranges. permissive() should be reserved for
operator-controlled asset lists.
The example server explicitly allows its own HTTP hostname so configured mosaics
can read its /data assets. Other hosts use the default policy; there is no
environment override. A service that accepts untrusted mosaic or STAC documents should
construct MosaicTiler itself and pass strict() through the builder as shown
above.
Presigned query strings are redacted from mosaic errors and log messages. The access policy is a URL-resolution guard, not a complete outbound proxy sandbox. Applications still own client timeouts, redirect policy, egress firewalling, document-size limits, and any logging they add around the library.
3 - Production operation
Share one cache
The default decoded-block cache budget is 512 MiB. A server with many readers
should inject the same Arc<BlockCache> into every reader and mosaic. Give
each distinct COG a stable source ID so cache entries cannot collide.
This budget covers resident decoded blocks in the cache, not total process
RSS. In-flight compressed bytes, decode and warp buffers, encoded responses,
reader metadata, and mosaic reducer state are additional memory. In particular,
Median retains every contributing image until reduction completes.
Default tile entrypoints share a RequestLimiter admitting 64 requests and a
512 MiB WorkBudget for active work, separate from decoded-cache residency.
Custom limiters and budgets can be injected. These accounting limits are not
a hard RSS ceiling: keep per-tile asset fan-out and retained output buffers
bounded, and size containers using representative load tests. See
runtime limits and source consistency for defaults,
bounded memory-pressure retries, cancellation and pinned source snapshots.
Use BlockCache::stats() to observe:
hitsandmissesdecodedandfetchedentriesresident_bytes
Useful derived signals include hit ratio, fetched-to-decoded amplification, and resident bytes divided by the configured budget.
Bound CPU work
Decode, stitch, warp, mosaic reduction, render, and encode are CPU-heavy.
They run in spawn_blocking behind CpuLimiter. Non-mosaic CogReader and
Tiler instances accept a caller-owned limiter. MosaicTiler shares one
internal limiter across its assets and final render, but does not accept the
non-mosaic limiter. Defaults follow the host’s available parallelism and clamp
zero to one permit.
Tiler::new reuses the limiter carried by its reader:
The example server’s CPU accounting depends on the path. A command-line default
COG uses separate reader-decode and tiler-render limiters, so those phases can
overlap. Lazily selected local datasets reuse one AppState limiter sized to
the larger configured value. MosaicTiler owns another shared limiter. Treat
the two environment values as component tuning, not as a universal
process-wide CPU cap. For non-mosaic readers and tilers, wire one
library-level CpuLimiter through every component that supports injection.
Current MosaicTiler owns an internal shared limiter and does not expose a
builder hook for injecting the non-mosaic limiter. A strict numeric CPU-job cap
across mixed mosaic and single-COG traffic is therefore unsupported today.
External request concurrency provides useful coarse backpressure, but is not
equivalent to one shared permit budget.
Do not lower tile concurrency preemptively. Measure P95 latency and CPU contention first; an unnecessarily small limit adds queueing to every tile.
Tune access patterns
- Increase cache capacity for multi-dataset servers or wide pan/zoom sessions.
- Prefer ZSTD-compressed COGs over DEFLATE when read latency matters.
- The example server prefetches a one-block ring by default. Set
ASYNC_GEOTIFF_PREFETCH_RING=0when extra object-store GETs are undesirable. - Keep
MosaicConfig::max_assets_per_tilebounded for untrusted indexes.
Export traces and metrics
Tiler::tile and CogReader::read_window emit tracing spans. Connect a
tracing-opentelemetry layer in the application when OTLP traces are needed.
The library intentionally takes no OpenTelemetry dependency.
Record request count, latency, error count, and the Ok(None) rate at the HTTP
or application boundary. Poll cache statistics separately for memory and
fetch behavior.
Protect remote access
Use a strict HttpAccessPolicy when mosaic or STAC documents are not trusted.
Configure credentials on an object_store instance rather than embedding them
in asset identifiers. Avoid logging raw presigned URLs at application
boundaries outside the library.
4 - Runtime limits and source consistency
Active work and memory ownership
Default readers, tilers and mosaic pools share WorkBudget::global(), a
512 MiB allowance. WorkBudget::new(bytes) creates an independent allowance;
clone and inject the same budget through ReaderConfig::work_budget,
Tiler::with_work_budget, and MosaicTiler::builder(...).work_budget(...)
when these objects must share an application limit.
Low-level reservations fail immediately with AgError::ResourceLimit. Public
tile/warp/mosaic requests release the failed attempt, wait for exclusive
admission, and retry once under the original deadline. Direct CogReader calls
remain fail-fast; individually oversized requests still fail. used_bytes(), peak_bytes() and limit_bytes()
expose the accounting. Cloned reservations stay live until their last owner is
dropped, including an already-running blocking job after its caller cancels.
Managed response buffers retain their byte reservation for the buffer lifetime.
This is an active-work allowance, not an OS RSS ceiling. Completed
caller-owned images, resident decoded caches, allocator overhead, runtime/TLS
state and unrelated application allocations have different ownership. Set
CacheConfig::max_bytes for decoded-cache residency, share caches deliberately,
and bound how many completed outputs the application retains.
Reader defaults add per-operation validation:
| Setting | Default |
|---|---|
| Source-window pixels | 16 × 1024 × 1024 |
| Window bytes / compressed fetch batch | 256 MiB |
| Blocks per window | 4096 |
| Declared decoded bytes per block | 64 MiB |
| Cumulative metadata bytes while opening | 8 MiB |
| IFDs | 256 |
| Entries per IFD | 4096 |
| Concurrent foreground windows | 8 |
Use ReaderConfig::default() and override fields explicitly. Oversized ranges,
metadata and arithmetic overflow fail before the corresponding fetch/allocation.
Background prefetch uses immediate budget acquisition and skips when foreground
work owns the allowance. These guards and a small workload are not a proof of
every codec/backend’s behavior on arbitrary input.
The default decoder registration now bounds the output of uncompressed, Deflate/OldDeflate, LZW, ZSTD and JPEG blocks against the declared block shape. Typed/planar byte arithmetic and JPEG MCU padding are included in validation and work estimates. This covers the registered implementations; it does not make an arbitrary caller-supplied codec implementation safe.
Request admission, deadlines and cancellation
The default RequestLimiter is shared across public tile calls and admits
64 requests. Excess calls return AgError::Overloaded immediately. Custom
limiters can be shared using Tiler::with_request_limiter and
MosaicTilerBuilder::request_limiter. Internal mosaic child work does not
consume a second request admission.
The default request timeout is 30 seconds, including I/O and permit waits.
TilerConfig and MosaicConfig expose max_in_flight_requests and
request_timeout; use ..Default::default() when constructing these configs.
tile_with_options (and single-source warp_tile_with_options) accepts a
RequestOptions containing a per-call timeout and a cloneable
RequestCancellation. Calling cancel() on any clone ends the request with
AgError::Cancelled; expiration returns AgError::DeadlineExceeded.
Cancellation is cooperative. It stops later pipeline stages and drops pending I/O, but does not preempt an arbitrary running CPU instruction or remote server handler. Blocking work retains its admission/accounting until it finishes. HTTP client/TLS initialization runs off async workers under a separate bounded limit; local workload telemetry distinguishes request return from later activity.
The example server exposes:
ASYNC_GEOTIFF_MAX_IN_FLIGHT_REQUESTS(positive integer).ASYNC_GEOTIFF_REQUEST_TIMEOUT_MS(positive integer milliseconds).
HTTP admission and timeout include dataset lookup/opening. Library callers that
open a CogReader directly outside a tile request should apply their own outer
timeout when an application deadline must include that initial open.
The server passes the configured timeout and a shared library request limiter
to default, lazy local, MosaicJSON and STAC tile paths. Raising the HTTP values
does not leave an additional fixed 30-second total request deadline or 64-request
admission cap. Transport connection/read timeouts remain independent. Running
CPU work retains its library admission until it drains after HTTP cancellation.
ASYNC_GEOTIFF_PREFETCH_RING applies to default and lazy local single-COG
readers; mosaic asset readers retain their zero-prefetch default.
HTTP mosaic asset error labels remove URL userinfo, query strings and fragments, including when opening is rejected by the access policy. Store-backed asset keys retain their existing semantics; they are not interpreted as HTTP URLs.
Mosaic ordering and selected bands
MosaicJSON assets retain their listed paint order during deduplication. Prefix lookups traverse matching quadkeys in lexical order and retain the first occurrence of each asset, preserving each key’s list order.
Mosaic selection comes from the Raster Style document, including
pixel_selection, stage, and the one-based rank_channel for Highest/Lowest.
Before channels, the input tuple contains deduplicated source dependencies sorted
by band number; after channels, it contains evaluated output channels. Every
component must be valid for the asset’s tuple to participate. Highest/Lowest
keep a complete tuple from the winning asset, with source order breaking ties.
Dependencies are warped independently so unrelated bands’ nodata cannot affect
interpolation. Gamma, color mapping and opacity run once after reduction.
Calibration other than none currently returns an error; the old unscale
parameter is removed. Public Tiler::warp_tile returns compact dependencies or,
for after_channels, evaluated channels. It no longer returns every source band.
Pinned object snapshots
Managed open_http and open_object_store use
SnapshotPolicy::RequireValidator by default. A strong ETag or immutable object
version must be available. Conditional reads and decoded-cache identity pin
metadata and pixels to that snapshot; a changed source produces an error
instead of a mixed-version tile. Version-aware stores may continue serving the
pinned old version. LocalFileSystem managed opens supply validators, so the
checked-in fixtures do not need to waive this contract.
An opaque ObjectReader does not expose its underlying store for conditional
requests. Passing it to .open(reader) therefore requires explicit
.snapshot_policy(SnapshotPolicy::AssumeImmutable). This is a caller guarantee,
not verified snapshot consistency. Use it only for a source the caller keeps
immutable, or migrate to a managed store open.
Validator consistency depends on truthful validators and the backend’s atomic write contract. It cannot detect a server that changes bytes while reusing the same validator, or raw in-place filesystem writes that violate that contract.
There is no automatic source refresh. Reopen a direct reader to discover the
current validator. For mosaics, call invalidate_source(key, source_id) so the
next lookup opens a fresh snapshot, or use a new source version identity.
Invalidation does not mutate an existing reader Arc: it retains its original
snapshot, and uncached reads fail if the old version is no longer available.
New reader/cache identities include the validator and size, preventing stale
decoded blocks from being reused as new imagery.
5 - Local workload and benchmarks
Run these tools from the Git checkout on macOS or Linux. They are development tools and do not run when an application depends on this crate.
Build the optimized binary separately from measurement:
Repeat with concurrency 1 or 8 and a different output filename. The wrapper rejects more than 128 samples or concurrency above 16 and kills a run after 120 seconds. All network traffic goes to an ephemeral loopback server in the workload process. No credentials or public datasets are required. The driver calls the library tile APIs directly; the HTTP server supplies TIFF bytes. This is not an end-to-end load test of the example server’s tile routes.
The 64×64 synthetic Deflate TIFF has a 32×32 overview, three bands, four base blocks and a nodata corner. The server adds 2 ms to each GET. Cold samples include reader/client creation and metadata; warm samples use an already rendered tile’s reader and decoded blocks. Mosaic samples use three separately identified URLs with identical fixture pixels and the Mean reducer. Outputs are 64×64 PNGs. Samples run in bounded batches; throughput includes batch barriers and is not an open-loop arrival-rate test.
Each latency table reports nearest-rank P50/P95/P99. With 32 samples, P99 is the maximum sample; do not interpret it as a stable production tail estimate. The JSON includes cache counters before/after, individual cold cache snapshots, GET counts, prepared response bytes, maximum active server handlers, and the global work-budget estimate (its peak is cumulative across scenarios). “Response bytes” means payloads prepared by the server, not bytes proven delivered to a cancelled client.
Cancellation cases use a 100 ms server delay. One case aborts task futures; others use an explicit cancellation handle (20 ms) or a deadline (10 ms). The latter launch eight requests against an admission limit of two and record all rejection/termination outcomes. Single-source readers are opened first so these cases exercise in-flight tile reads. Cold mosaic cases additionally cover client/metadata setup. Counters are sampled at return and after a 250 ms settling period; admitted-request permits and work-budget bytes are recorded. Remote server handlers may finish after client cancellation. These counters do not measure every background TLS initialization job or prove OS thread preemption of a running decoder.
A memory-admission case submits eight 1024×1024 renders to a 64 KiB work budget. It requires eight resource-limit errors, no pixel GETs, and zero retained work bytes afterward. This checks early rejection, not peak memory under a full budget.
The wrapper records UTC time, OS/architecture, logical CPU count, Rust version,
commit and dirty status, executable SHA256, fixture SHA256, build-profile label,
and child user/system CPU time. Peak RSS is the workload process’s wait4
ru_maxrss (bytes on macOS; KiB converted on Linux); compiler memory is excluded.
It is a whole-run peak, not a per-scenario value or a process-tree total.
These are bounded synthetic measurements on one machine. They do not establish production capacity, an adversarial codec memory bound, or behavior under an unbounded request stream. Fixture regeneration uses Python’s standard library; Deflate bytes also depend on the installed zlib implementation.
Resampler microbenchmark
The six cases compare contiguous and strided bilinear sampling, each with and without a mask, plus masked and unmasked Average downsampling. This measures kernel cost, not HTTP latency or cross-CRS pipeline throughput. Criterion is a development dependency and is not a dependency of library consumers.