Reading Zarr Datacubes from S3 with fsspec
Open the store with an s3:// URL, select lazily, and load only what you need:
import xarray as xr
cube = xr.open_zarr("s3://example-bucket/cubes/ndvi.zarr",
consolidated=True,
storage_options={"anon": False, "profile": "research"})
summer = cube.ndvi.sel(time=slice("2026-06-01", "2026-08-31")).median("time").compute()
Nothing is downloaded until .compute(), and then only the chunks the selection touches. This page belongs to working with Zarr and cloud-native datacubes in Core Raster Fundamentals & STAC Mapping.
The Layers Between Python and the Bucket
storage_options is passed through to the fsspec layer unchanged, which is why the same argument handles credentials, anonymous access, custom endpoints for S3-compatible stores and request timeouts. Understanding that separation makes most connection problems easy to place: an authentication error lives in the bottom two layers, a shape or dtype error in the middle ones.
Environment & Setup
| Package | Version pin | Used for |
|---|---|---|
xarray |
>=2024.1 |
Opening and selecting from the cube |
zarr |
>=2.17 |
The store format |
fsspec |
>=2024.2 |
The filesystem abstraction and caching wrappers |
s3fs |
>=2024.2 |
The S3 implementation behind s3:// URLs |
dask[array] |
>=2024.1 |
Lazy, parallel chunk reads |
pip install "xarray>=2024.1" "zarr>=2.17" "fsspec>=2024.2" "s3fs>=2024.2" "dask[array]>=2024.1"
Complete Working Example
import fsspec
import xarray as xr
def open_cube(url: str, *, anon: bool = False, profile: str | None = None,
endpoint_url: str | None = None, cache_dir: str | None = None) -> xr.Dataset:
"""Open a remote Zarr cube with optional local chunk caching."""
so: dict = {"anon": anon}
if profile:
so["profile"] = profile
if endpoint_url: # S3-compatible stores
so["client_kwargs"] = {"endpoint_url": endpoint_url}
if cache_dir:
# 'filecache' keeps fetched objects on local disk across sessions
mapper = fsspec.get_mapper(
f"filecache::{url}",
s3=so,
filecache={"cache_storage": cache_dir},
)
return xr.open_zarr(mapper, consolidated=True)
return xr.open_zarr(url, consolidated=True, storage_options=so)
if __name__ == "__main__":
cube = open_cube("s3://example-bucket/cubes/ndvi.zarr", profile="research",
cache_dir="/tmp/zarr-cache")
print(cube) # metadata only, no chunk reads
point = cube.ndvi.sel(x=780_050, y=9_950_050, method="nearest")
series = point.load() # fetches only the needed chunks
print(series.to_series().describe())
The filecache:: prefix is fsspec’s chaining syntax: it wraps the S3 filesystem in a local disk cache, so a chunk fetched once is served from disk thereafter. For interactive work that revisits the same area repeatedly — tuning a model on one region, say — it turns the second and later runs from network-bound into disk-bound, which is typically ten times faster.
Where the Time Goes
After the open, remote reads are governed by two things: how many chunks a selection touches, and how far the compute is from the bucket. The first is a property of the chunk shape, covered in choosing chunk shapes for Zarr raster cubes. The second is a property of where the code runs, and it is the one people most often ignore.
Keep the Compute Next to the Data
A laptop reading a terabyte-scale cube across the internet will work — every chunk arrives eventually — but each request pays tens of milliseconds of latency and every byte is billed as egress. The same computation on a worker in the bucket’s region pays a few milliseconds per request and nothing for transfer.
The pattern that scales is to push the computation to the data and pull back only the reduced result: run the seasonal median or the trend fit on a cluster in the storage region, write the small output, and download that. The execution side of this is the subject of scaling raster processing with Dask, and the cost side of reducing S3 egress costs in raster pipelines.
For interactive exploration from a laptop, the local file cache above is the pragmatic compromise: the first look at an area is slow, every later look is fast, and the cache directory can be deleted when the work moves elsewhere.
Verification
import dask.array as da
cube = open_cube("s3://example-bucket/cubes/ndvi.zarr", profile="research")
assert isinstance(cube.ndvi.data, da.Array), "variable was loaded eagerly"
assert cube.rio.crs is not None, "no CRS on the remote store"
print(cube.time.values.min(), "to", cube.time.values.max(), f"({cube.sizes['time']} dates)")
It is also worth printing the store’s own chunk encoding at this point, since a remote store built by someone else may be shaped for a different access pattern from yours, and knowing that before the first heavy computation saves a slow surprise. The Dask check is the one worth keeping in any shared notebook. xr.open_dataset(engine="zarr") without chunks can load eagerly, and on a remote cube that is a silent multi-gigabyte download that looks, from the outside, like the notebook hanging.
Common Errors
NoCredentialsError on a public bucket
The filesystem is looking for credentials the bucket does not need. Pass storage_options={"anon": True}.
KeyError: '.zmetadata'
The store is not consolidated. Open with consolidated=False, and consolidate it if you can write to it.
Reads hang with no error
The request is being retried against an unreachable endpoint, often a wrong region or custom endpoint. Set the endpoint explicitly and a timeout in client_kwargs.
The cache directory grows without bound
filecache never evicts. Point it at a scratch location and clear it between projects.
Reads work locally but fail on cluster workers
The credentials exist only in the notebook’s environment. Workers need them too — through an instance role, environment variables set on the cluster, or explicit keys in storage_options that travel with the task graph.
Every read re-downloads the same chunks
The cache is wrapped around the wrong layer or keyed by a URL that changes. Use a stable URL and the filecache:: chaining shown above, and confirm the cache directory is actually filling.
Frequently Asked Questions
Q: How do I read a public Zarr store without credentials? Pass storage_options={‘anon’: True}. Without it, the S3 filesystem looks for credentials, fails to find any and raises, even though the bucket needs none.
Q: Why does opening a remote store take so long? Usually because metadata is not consolidated, so the reader fetches one small file per array and coordinate. With consolidated metadata the open is a single request; ask the store’s owner to consolidate or do it yourself if you control it.
Q: Is it expensive to read a large cube from another region? It is slower, because every chunk request pays cross-region latency, and often billed as inter-region transfer. Run the computation in the store’s region and bring back only the reduced result.
Q: Does the same approach work on Azure or Google Cloud Storage?
Yes. fsspec provides equivalent implementations for both, addressed with their own URL schemes, and the same storage_options pattern carries their credentials. Only the scheme and the keys inside the options change.
Related
- Working with Zarr and Cloud-Native Datacubes — the parent topic.
- Reading COGs from Azure Blob and GCS — the GeoTIFF counterpart across clouds.
- Choosing Chunk Shapes for Zarr Raster Cubes — what decides how many requests a read needs.
- Passing Credentials to Remote Raster Workers — getting these storage options onto a cluster.