How it works
A Zarr array is stored in chunks, and its chunk_grid says where the cuts are. A
regular grid has one chunk length per axis. A rectilinear grid has a list of lengths per
axis, so the cuts along an axis can fall anywhere, but every cut still runs the whole way across the
array: the grid is the cross product of one list per axis, the same tuple of tuples dask has always
used for its own chunks. Finding the chunk that holds an index is a division on a regular axis and a
search of the running sums on a rectilinear one (point at the bands on the right to see both).
In zarr.json the rectilinear grid is {"name": "rectilinear", "configuration": {"kind":
"inline", "chunk_shapes": [...]}}, one entry per axis: an integer (a regular axis), a list of
lengths, or [length, count] runs, which can be mixed. The panel shows the
chunk_grid of what is drawn. Left, the array in 3D with the chunks pulled apart, the ones a
read touches in yellow and the part it wants in orange; right, the cuts along the first axis for
both grids, or a mosaic's tiles and the grid their edges make.
The last chunk
When the chunk length does not divide the array's length, a regular grid overhangs the end:
there are ceil(n / c) chunks and the last one reaches past the array. Zarr v2 says the contents of
that overhang are undefined; Zarr v3 says border chunks always have the full chunk size, and
recommends writing the fill value there. A reader decodes the whole chunk, fill and all.
This is the general case of the rectilinear grid: a regular grid is exactly a rectilinear grid with
an integer for every axis. The rectilinear grid can also say what is left, [[30, 3], 10]
for 100 steps in 30s, and then the last chunk is stored at 10, not padded to 30. Its lengths must add
up to at least the array's length, so the last chunk may still overhang (and even whole chunks past
the end are allowed, which is what a shrunken array keeps). Only the last chunk on an axis can
overhang: every chunk before it holds exactly its listed length.
Days in months, days in years
Months have 28, 29, 30 or 31 days and years have 365 or 366, so daily data chunked one month or
one year at a time cannot be a regular grid. With a regular 30-day chunk, February 2024 takes two
chunks and half the values read are not wanted; with monthly chunks it is one. A leap year is
[31, 29, 31, 30, 31, 30, [31, 2], 30, 31, 30, 31], and years of daily data from 2000 are
[366, [365, 3], 366, [365, 3], ...]. This was the leading example in ZEP0003.
The same grid is what you get by appending: each day (or batch) added becomes a new chunk of its
own length, where a regular grid would have to rewrite its part-filled last chunk. A time series at
one pixel reads every chunk along time either way: what matters there is how the chunks are cut
across the map.
Files, and the chunks inside them
The strongest reason for rectilinear grids is virtual references: kerchunk, VirtualiZarr and
Icechunk describe a pile of netCDF, HDF5 or TIFF files as one Zarr array whose chunks are byte ranges
in the original files. The files' own blocks cannot be changed, so the grid has to fit them. With a
regular grid, a file whose length is not a multiple of the chunk leaves a short chunk in the middle
of the array; kerchunk refuses it ("irregular chunking at its boundary. This is only allowed for the
final array") and VirtualiZarr allows it only on the last file. A rectilinear grid can list each
file's chunks, short ones included (orange in the bands).
The catch is padding. HDF5 (so netCDF-4) and tiled TIFF store a file's last chunk at the full chunk
size, padded, and a rectilinear chunk is stored at its listed length, so a padded 65-day remainder
stored as 100 cannot be referenced as a chunk of 65. Proposals to let a chunk's stored size differ
from its length were raised in the rectilinear extension's review and the ZEP meetings, but are not
in the spec. Sources that do not pad (TIFF strips, netCDF classic records, files whose length is a
multiple of their chunk, or one chunk per file) fit cleanly.
Mosaics: uneven, staggered and overlapping tiles
Uneven tiles. Because there is one list of lengths per axis, every chunk in a column has the
same width and every chunk in a row the same height. Tiles of different sizes fit as one chunk each
only if they line up in rows and columns. A tile can be several chunks (its own internal blocks, as
runs of lengths), so long as those blocks are exact.
Staggered tiles. When rows of tiles are offset, like bricks, every edge cuts the whole
array, so the grid that holds them is cut at every tile's edge and most tiles become two or more
chunks. For virtual references that only works where those cuts fall on the files' own block
boundaries; otherwise the bytes have to be rewritten.
Overlap. A chunk grid is a partition: every index is in exactly one chunk, regular or
rectilinear. Two scenes that cover the same pixels cannot both be chunks of one array, so overlap
(red) has to be settled before Zarr sees it, by a rule such as GDAL VRT's "values will be taken from
the last overlapping input", by warping to a new grid, or by cropping each tile so the footprints
meet without overlap. Overlapping or haloed chunks have come up in Zarr discussions (they would need
an order, such as timestamps), but nothing has been proposed. HDF5 virtual datasets and GDAL VRT,
which place sources at any offset, are the nearest things.
Inside one TIFF or netCDF file
Within one file, every array is a regular grid with an overhanging last block. Neither format
has variable block sizes within one array: they appear across files, or across a COG's overviews,
each a regular grid of its own.
| In one file | Blocks | Last block |
| TIFF tiles | one TileWidth x TileLength per image | full size, padded |
| TIFF strips | one RowsPerStrip, full width | short, not padded |
| COG overviews | a tiled image per level | padded, per level |
| netCDF classic | record variables: 1 along the record dimension | none |
| netCDF-4, HDF5 | one chunk shape per variable | full size, padded |
The exceptions are rare: TIFF-FX's StripRowCounts gives strips of different heights, and an HDF5
virtual dataset stitches other datasets of any shape. So both formats have the same style of chunks
as a regular Zarr grid, and a rectilinear grid helps only when files are put together.
Where it stands, October 2026
ZEP0003 (Martin Durant and Isaac Virshup, 2022) proposed variable chunking for Zarr and is
still a draft. The rectilinear chunk grid extension, by Davis Bennett, building on ZEP0003 and
prototyped with Lachlan Deakin at the 2025 Zarr summit, was merged into zarr-extensions in March
2026. Support:
| Library | Rectilinear |
| zarr-python | 3.2.0 on, behind array.rectilinear_chunks |
| zarrs (Rust) | 0.23.8 on |
| Icechunk | 2.0.0 on |
| xarray | reading, 2026.09.0 on |
| VirtualiZarr, Zarr.jl | open pull requests |
| GDAL, tensorstore, zarrita.js, netCDF-C | regular only |
Zarr arrays read from a URL here are drawn from their zarr.json alone; no chunk is read. Core in
src/core/chunkgrid.js, checked in test/check-chunkgrid.mjs (including the
extension's own example).
Hash keys: case, grid (reg, rect), c (regular chunk, or the
chunk inside each file), a and w (the read), where (map,
pixel), ex, cam.
- Durant, M. and Virshup, I. (2022) ZEP0003:
Variable chunking, draft.
- Bennett, D. (2026) Rectilinear
chunk grid, zarr-extensions.
- Zarr v3 core specification, regular
chunk grid; Zarr v2 specification.
- Hamman, J., Jones, M. and Bennett, D. (2026) Variable
length chunks in Zarr, Earthmover blog.
- zarr-python #3802 (Max Jones),
zarrs #284 (Lachlan Deakin), Icechunk
#1806 (Deepak Cherian), VirtualiZarr
#954 (Max Jones, Tom Nicholas),
kerchunk #374 (Isaac Virshup).
- Discussion: ZEP0003 discussion,
zarr-specs #138,
zarr-extensions #25.
- Adobe (1992) TIFF Revision 6.0; OGC Cloud
Optimized GeoTIFF standard; Unidata, netCDF
file format specifications; The HDF Group, HDF5 chunking and virtual datasets.
- GDAL gdalbuildvrt.