Chunk shape decides how much you actually read
3 min read · Updated July 29, 2026
HDF5 and Zarr both store large arrays as a grid of fixed-size chunks. Each chunk is compressed and stored as one unit, which means it is also the smallest thing that can be read. You cannot read half a chunk: to get one element, the library fetches, decompresses and discards the whole chunk it lives in.
Read amplification is the ratio between bytes actually read and bytes wanted. It is entirely determined by the relationship between chunk shape and access pattern, and it is the single largest lever on read performance for both formats.
The same array, two access patterns
Take a 2-D array chunked into wide, short blocks, a common default when data arrives row by row. Reading one row touches a single row of chunks. Reading one column touches every chunk in the array.
Nothing about the array changed. The second read is slower by roughly the number of chunks along the row axis, because that is how many extra chunks it had to decompress to reach the elements it wanted.
| Array | Chunk shape | Read | Bytes wanted | Bytes read | Amplification |
|---|---|---|---|---|---|
| 10000 × 10000 float32 | 10000 × 1 | one row | 40 KB | 400 MB | 10000× |
| 10000 × 10000 float32 | 10000 × 1 | one column | 40 KB | 40 KB | 1× |
| 10000 × 10000 float32 | 500 × 500 | one row | 40 KB | 20 MB | 500× |
| 10000 × 10000 float32 | 500 × 500 | 500 × 500 tile | 1 MB | 1 MB | 1× |
Picking a chunk shape
- Start from the access pattern, not the array. Whatever you slice most often should be contained in as few chunks as possible. If you read time series along the time axis, the time axis should be long in the chunk; if you read spatial snapshots, it should be short.
- Target roughly 1 to 10 MB per chunk, uncompressed. Below about 1 MB, per-chunk overhead and metadata dominate, especially on object storage, where each Zarr chunk is a separate request. Above about 10 MB, a small read wastes a lot of decompression.
- Prefer chunks that divide the shape evenly. Partial edge chunks are stored at full size in both formats, so a 1000-long axis chunked at 300 wastes most of the fourth chunk.
- Check the number of chunks. A 100,000-chunk Zarr store is 100,000 objects to list and fetch; a 100,000-chunk HDF5 file has a correspondingly large B-tree index to traverse.
import h5py, numpy as np
# Row-major reads: keep whole rows together.
with h5py.File("out.h5", "w") as f:
f.create_dataset("x", shape=(10000, 10000), dtype="f4",
chunks=(64, 10000), compression="gzip")
# Zarr v3: the same decision, different spelling.
import zarr
z = zarr.create_array(store="out.zarr", shape=(10000, 10000),
chunks=(64, 10000), dtype="f4")The HDF5 chunk cache
HDF5 keeps decompressed chunks in a per-dataset cache. As of HDF5 2.0 that cache defaults to 8 MiB with 8191 slots; releases before 2.0 defaulted to 1 MB with 521 slots. A single chunk larger than the cache is never retained, so a loop that revisits it re-reads and re-decompresses it every time, a common cause of "HDF5 got slower when I increased chunk size".
import h5py
# Size the cache to hold the chunks a loop will revisit.
with h5py.File("out.h5", "r", rdcc_nbytes=256 * 1024**2, rdcc_nslots=10007) as f:
x = f["x"]
for i in range(x.shape[0]):
row = x[i, :]Checking what a file already uses
Chunk shape is fixed at creation and cannot be changed in place; rechunking means rewriting. The first step with an unfamiliar file is reading its existing chunk shape: dataset.chunks in h5py, the chunk_grid field of zarr.json in Zarr v3, or the dataset inspector in the HDF5 viewer and Zarr viewer, which show chunk shape and compression alongside the array shape.
Sources
- HDF Group: Dataset storage layout and chunking
- h5py: Chunked storage
- HDF5: H5Pset_chunk_cache (default cache size and slot count)
- Zarr: Optimizing performance (chunk size and shape guidance)
Related articles
Inspect these files
ViewKit opens these formats in the browser. The file is parsed locally and never uploaded.