product · icechunk
transactional storage for tensors
Icechunk is a Zarr-compatible open-source transactional storage engine for tensors — the foundation for your multi-modal scientific database.
trusted by open-source adopters










why tensors?
Scientific data doesn't fit in rows and columns
"Structured data" usually means tabular data — rows, columns, the relational model. But what makes data structured is really that it conforms to a data model that eliminates redundancy. The relational model is one such model. For data about the physical world — weather, climate, satellite imagery, microscopy, genomics — the right model is the multidimensional array, or tensor.
That model has a name: the Unidata Common Data Model, which underlies NetCDF and HDF5, and there are exabytes of data in the wild that use it. A dataset is a set of variables, each an array with a homogeneous type and named dimensions, indexed by small coordinate variables. The coordinates are stored parsimoniously — one list per dimension, not one value per cell.
3,481
coordinate values needed for a 5 TB weather forecast dataset in the array data model — under a megabyte
964 billion
coordinate values for the same data flattened into a table, where every coordinate repeats on every row
Compression can hide some of that duplication, but it can't remove it from the data model — and in a table, locating a single point in space and time means scanning every row. Keep the dimensions and you keep efficient indexing; flatten them and you pay for it on every query.
The formats predate the cloud
NetCDF, HDF5, GRIB, and TIFF were designed for filesystems, where seeking is nearly free. Object storage is different: there is no "open a file", only requests over the network, each carrying tens of milliseconds of latency. Because these formats scatter their metadata throughout the file, simply discovering what's inside one means a long sequence of dependent requests. In one benchmark, reading just the metadata of a single 1 GB NetCDF file on S3 took 502 requests to move 267 KB — an effective 7 KB/s, slower than dial-up. And that's one file; real datasets have thousands.
Zarr: Parquet for arrays
Zarr keeps the array data model and fixes the layout for object storage. The metadata lives in its own known location, so one request tells you everything about the dataset's structure. The arrays are split into chunks, each an independently addressable object, so readers fetch exactly the chunks a query touches — thousands of them in parallel, saturating the enormous throughput object storage offers. Groups nest like directories, and every level carries its own metadata, so datasets stay self-describing and lazily navigable.
Zarr gives you the format. What it doesn't give you is a database — and that's where Icechunk comes in.
why icechunk?
Zarr is a great format. It's not yet a database.
Zarr is a cloud-optimized format for multidimensional arrays that is a significant advance over legacy formats like NetCDF, GRIB, and TIFF. However, Zarr has three critical limitations:
-
Safety — You can accidentally delete or corrupt your data in an unrecoverable way.
-
Consistency — Concurrent readers and writers can see partial, inconsistent data.
-
Reproducibility — Data modifications happen silently with no version tracking.
These limitations mean Zarr cannot be reliably used as a database — a critical capability for modern data-intensive workflows like weather forecasting, climate modeling, and geospatial analysis, where multiple teams need to safely read and update shared datasets.
Icechunk provides git-like snapshots combined with ACID transactions. Every write operation creates an immutable snapshot, while branches and tags provide familiar version-control semantics:
-
Safety — Recover from corrupted or accidentally deleted data using version history.
-
Consistency — Readers always see complete, valid snapshots — never partial writes.
-
Reproducibility — Reference any version of your data permanently via commits or tags.
# Write and commit atomically - readers never see partial updates
with repo.transaction("main", message="Add new forecast") as store:
group = zarr.open_group(store)
group["temperature"][:] = new_temp_data
group["pressure"][:] = new_pressure_data
# Both arrays updated together or not at all
# Time travel to any previous version
historical_session = repo.readonly_session(snapshot_id="abc123")
# Reproducible analysis with permanent references
repo.create_tag("v1.0-release", snapshot_id="abc123") # Immutable reference
session = repo.readonly_session(tag="v1.0-release") safety
Never lose data again
No matter how careful you are, it is always possible to unintentionally write to the wrong chunk or update an existing chunk with corrupted data. With Icechunk, you cannot permanently destroy or corrupt your data — if you commit bad data, you can always revert using version history.
import zarr
# Open existing group
root = zarr.open_group("my_data", mode="a")
# Accidentally overwrite critical data
root["temperature"][:] = corrupted_data
# Data is permanently lost! 💥
# No way to recover the original values import icechunk as ic, zarr
repo = ic.Repository.open(storage)
with repo.transaction("main", message="Update temp") as store:
root = zarr.open_group(store)
root["temperature"][:] = corrupted_data
# Oh no! The data was corrupted. But we can recover:
previous_snapshot = list(repo.ancestry("main"))[1].id
repo.reset_branch("main", previous_snapshot)
# Data restored! ✅ storage space
Version history without ballooning costs
Icechunk is storage-optimal: each commit stores only the chunks that changed. And if maintaining the full history is too much, old commits and branches can be expired via Icechunk's built-in garbage collection.
On plain cloud storage a new "version" means a fresh copy of every chunk. Below, each commit writes only its changed chunks — every other chunk in the new snapshot keeps pointing at the object already in storage.
consistency
Readers never see partial writes
The output of a computational model must be internally consistent: if the pressure variable has one more time step than air temperature, analysis pipelines break. With plain Zarr, variables are never written at exactly the same time, so concurrent readers can see inconsistent data — a serious problem for latency-critical applications like emergency weather monitoring and trading on forecasts.
Icechunk is, by design, always consistent: ACID transactions guarantee readers see either all old values or all new values, never a mix. More in the Multi-Player Mode blog post.
Watch it happen. A writer overwrites the cat chunk by chunk in random order, while two readers read all the data they can see at any moment. Reads and writes are not instant, so a read can overlap a write — and Zarr and Icechunk differ in what the reader sees during a write.
Zarr: a read that starts mid-write returns a mix of old and new chunks — the state of the data is inconsistent, and the cat is neither alive nor dead. Icechunk: writes go to a session that is invisible to readers until it is committed, so a reader never observes partially-written data. The cat is either alive or dead.
Writer
Zarr Reader
Icechunk Reader
# Writer process
forecast["temperature"][:, new_time] = temp_data # Written
forecast["pressure"][:, new_time] = press_data # Written
# ... still writing ...
# Reader process running at the same time
temp = forecast["temperature"][:, new_time] # ✓ Gets new data
pressure = forecast["pressure"][:, new_time] # ✓ Gets new data
humidity = forecast["humidity"][:, new_time] # ✗ Gets old data!
# Analysis breaks: variables are from different time snapshots # Writer process
with repo.transaction("main", message="Add forecast") as store:
forecast = zarr.open_group(store)
forecast["temperature"][:, new_time] = temp_data
forecast["pressure"][:, new_time] = press_data
forecast["humidity"][:, new_time] = humid_data
# All variables updated atomically
# Reader process (concurrent with writer)
session = repo.readonly_session("main")
forecast = zarr.open_group(session.store)
# Always sees a complete, consistent snapshot
# Either all old values OR all new values, never mixed reproducibility
Pin any analysis to an immutable version
Weather datasets regularly add new timepoints — and backfill and fix errors in prior ones. Zarr has no mechanism for tracking when data changed, creating a reproducibility crisis for any downstream analysis. Icechunk requires a commit for every change, so any version can be referenced permanently by commit or tag — making analyses reproducible even on datasets you don't control.
# January: Run analysis for paper
forecast = zarr.open_group("s3://public-weather/forecast")
results_jan = analyze(forecast["temperature"][:])
# Published results based on this data
# March: Try to reproduce results
forecast = zarr.open_group("s3://public-weather/forecast")
results_mar = analyze(forecast["temperature"][:])
# Data was updated! Results don't match!
# No way to know what changed or get back to January's data # January: Run analysis using specific tagged version
import icechunk as ic
repo = ic.Repository.open(
ic.s3_storage(bucket="public-weather", prefix="forecast")
)
session = repo.readonly_session(tag="2025-01-15")
forecast = zarr.open_group(session.store)
results_jan = analyze(forecast["temperature"][:])
# Record in paper: "Analysis used tag 2025-01-15"
# March: Reproduce results using same tagged version
session = repo.readonly_session(tag="2025-01-15")
forecast = zarr.open_group(session.store)
results_mar = analyze(forecast["temperature"][:])
# Exact same data, guaranteed reproducible results! ✓ zero-copy ingestion
Bring your archives — without copying them
There is a lot of archival data out there in formats like NetCDF, HDF5, GRIB, and TIFF. Icechunk supports virtual chunks: any chunk in a dataset can reference bytes inside existing archival files, loaded directly from the original source without copying or modifying them. With VirtualiZarr, a pile of legacy files becomes a single analysis-ready dataset — transactional, versioned, and queryable like any other Icechunk repo.
Below: to read a small slice with a native reader you download whole files and throw most of the bytes away. With virtual chunks, a tiny manifest maps every logical chunk to a (path, offset, length) in the archive — the reader pulls the manifest first, then range-gets only the bytes it needs.