Open-source project
markusressel/zfs-inplace-rebalancing avatar
markusressel/zfs-inplace-rebalancing

zfs-inplace-rebalancing: copying files to redistribute them across new vdevs

Simple bash script to rebalance pool data between all mirrors when adding vdevs to a pool.

790 stars46 forksShellCC0-1.0

At a glance

What is it?
A bash script that rebalances a ZFS pool after new vdevs are added by copying each file to a temporary name, deleting the original and renaming the copy, which spreads the blocks without needing a second pool, at the cost of rewriting the entire dataset and a set of documented preconditions.
Who is it for?
Adopt zfs-inplace-rebalancing only if you are running an older ZFS than 2.3.3, cannot provision a second pool, and have verified that your data is cold, snapshot-free and deduplication-free as the documentation requires. Do not reach for it on a current ZFS, because the project itself points at the zfs rewrite command as the better option that avoids most of the caveats.
Can I use it commercially?
Yes. CC0-1.0 is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
Is it still maintained?
Yes. The repository last received commits 125 days ago.
What is it written in?
Mainly Shell, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on October 24, 2026, and from our analysis. They are not legal advice.

Editorial analysis

Check zfs rewrite first, because the author tells you to

The most important thing in this repository is a warning at the top of the README, and it should shape any decision to use the script.

ZFS 2.3.3 introduced the `zfs rewrite` command, which is meant to address this very issue as well, but avoids most of the caveats the script in this repository has. The documentation says to definitely check it out first before trying to make this script work for you, and links both the upstream pull request and the manual page for `zfs rewrite`.

That is an unusual position for a project to take: the author is telling you to use the upstream feature instead of his script when your ZFS version has it. It also tells you the script's age and its provenance, since `zfs rewrite` arrived in 2026 and the script predates it.

The problem being solved is real. When you add vdevs to a ZFS pool, the new devices start out empty and the existing data stays where it was, so the pool's capacity is unevenly distributed. The conventional fix is to create a second pool, copy the data across, and destroy the old one, which needs enough additional hardware to hold a full copy of the data.

This script removes that requirement. The term in place is used to make it clear that no additional pool, and therefore no additional hardware, is necessary.

The mechanism is copy, delete, rename

The working principle is simple enough to read the script and understand. The script recursively traverses all the files in a given directory. Each file is copied with a `.balance` suffix, retaining all file attributes. The original is then deleted and the copy is renamed back to the name of the original file.

Why that works is a property of ZFS write allocation. When copying a file, ZFS spreads the data blocks across all vdevs, which effectively distributes the data of the original file more or less evenly. So the rebalancing happens as a side effect of ordinary writes, and no new pool is needed.

The documentation is careful about the limits of that effect. The way ZFS distributes writes is not trivial, which makes it hard to predict how effective the redistribution will be, and it links two write-ups on how ZFS allocates writes according to free space per vdev rather than latency per vdev, and on write allocation in the 0.7.x releases. In other words, expect the result to be more even than before and not perfectly even, and do not expect a guarantee.

The script is also explicit that this is not entirely in place, since a file has to be fully copied before the original is deleted. The consequence is a hard requirement: you have to have enough space to create a copy of the biggest file in your target directory for it to work. At no point in time are both versions of the original file deleted, which is the safety property that makes the process survivable.

Hardlinked files get a separate, more careful path

Files that share an inode need their own handling, and the script has a distinct path for them.

When the script detects an inode group of hardlinked files, it proceeds to copy one file in the inode group. The original file and all hardlinks are then deleted, the copy is renamed back to the name of the original file, and new hardlinks are generated from that copy to replace all other linked files that were removed.

The point of that sequence is to preserve the shared-inode relationship rather than silently turning a set of linked files into independent copies. If a user has a media library where the same file is linked from several directories, breaking the links would change the storage semantics and could change what the user sees when they delete one copy.

The script also verifies before it destroys. To make sure file attributes, permissions and file content are maintained when copying the original file, all attributes and the file checksum are compared before removing the original file, and that comparison can be switched off with `--checksum false`.

The hardlink path creates temporary files in the script location alongside the database file, and those are removed at the end of each run. The script also maintains three list files: `files_list.txt` lists all files found in the given target location, `sorted_files_list.txt` lists all files sorted by inode number, and `grouped_inodes.txt` lists all files by inode with all files from a given inode space separated on one line.

A text database keeps track of what has been rebalanced

Since file attributes are fully retained, it is not possible to verify if an individual file has been rebalanced. The script therefore keeps its own record, in a database file in its working directory called `rebalance_db.txt`, which can be disabled with `--passes 0`.

The format is two lines of text per processed file: one line for the file path and the next line for the current count of rebalance passes.

text
/my/example/pool/file1.mkv
1
/my/example/pool/file2.mkv
1

All files in a given inode group are added to the database when processed, and the highest count in a given inode group of files is used to determine if the group should be skipped when processing against the number of passes in a given script execution. That is the mechanism behind the pass counter: run the script repeatedly and it will skip the groups that have already had the requested number of passes, rather than rewriting the same data repeatedly.

This is a pragmatic answer to a real limitation. ZFS does not expose per file allocation information, so there is no way to check afterwards whether a particular file ended up spread across the pool. The database is the only record, which means it is also the only thing standing between you and re-running the whole operation on data that has already been processed.

Deduplication, snapshots and hot data all break the approach

The prerequisites section is where the script's real constraints are, and it is worth reading in full before anything else.

Deduplication must be off. Because of the working principle, which essentially creates a duplicate file on purpose, deduplication will most definitely prevent it from working as intended. If you use deduplication you probably have to resort to a more expensive rebalancing method that involves additional drives. In other words, the one thing that would make the copy cheap is the one thing that makes the copy fail.

The data must be cold. It is crucial that you only run it on data that is not actively accessed, since the original file will be deleted. That is a sharp edge: a file being read while the script replaces it can be read as a deleted inode, and the script is not a filesystem-aware rebalancer.

Snapshots change the arithmetic. If you take a snapshot of the data before running the script, ZFS now has to keep track of all of the data in the target directory twice, once in the snapshot and once for the new copy, which means you effectively use double the file size of all files within the target directory.

The balance status check that comes first is `zpool list -v`, and the guidance is to look at the difference in the `CAP` value, the `SIZE`/`FREE` versus `ALLOC` ratio, between vdevs. The sample output in the documentation shows a pool where a mirror reports 25 percent fragment and 53.1 percent capacity against a sibling mirror at 8 percent fragment and 50.9 percent, which is the kind of skew the script exists to address.

Running it, and the container that packages it

The repository is a single shell script, `zfs-inplace-rebalancing.sh`, with a `test/` directory, a `testing.sh` and a `Dockerfile`.

The Dockerfile is a thin wrapper. It builds on `phusion/baseimage:jammy-1.0.1`, installs `bc` with apt, copies the script in, and sets it as the entrypoint:

dockerfile
FROM phusion/baseimage:jammy-1.0.1
MAINTAINER markusressel

RUN apt-get update \
&& apt-get -y install bc \
&& apt-get clean && rm -rf /var/lib/apt/lists/*

COPY zfs-inplace-rebalancing.sh ./

ENTRYPOINT ["./zfs-inplace-rebalancing.sh"]

The image exists to run the script in a container with the dependencies present, which is convenient for testing and for running the script against a mounted pool path. The `bc` package is there for the arithmetic the script does on sizes.

The documentation does not give a full command line reference for the script in the visible portion, but it does name the two flags that matter most: `--checksum false` to skip the attribute and checksum comparison before deleting the original, and `--passes 0` to disable the database entirely. Both of those are switches that remove safety features, which tells you something about which defaults you should keep.

On maintenance, the repository is not archived, the last push was on 2026-06-22, and there are no GitHub releases. The licence is CC0-1.0, a public domain dedication, which for a shell script is an unusual and permissive choice.

Editorial conclusion

Adopt zfs-inplace-rebalancing only if you are running an older ZFS than 2.3.3, cannot provision a second pool, and have verified that your data is cold, snapshot-free and deduplication-free as the documentation requires. Do not reach for it on a current ZFS, because the project itself points at the zfs rewrite command as the better option that avoids most of the caveats. Verify first that you have free space for a copy of the largest file in the target directory, since the process is not truly in place, and read the caveats on snapshots and deduplication before you start.

Frequently asked questions

What does zfs-inplace-rebalancing do?

It is a bash script that rebalances pool data between all mirrors when adding vdevs to a pool. Each file is copied with a .balance suffix, the original is deleted and the copy is renamed back, so ZFS spreads the data blocks across all vdevs without a separate backup pool.

Should I use zfs-inplace-rebalancing or zfs rewrite?

The project points you at zfs rewrite, introduced in ZFS 2.3.3, which addresses the same issue and avoids most of the caveats the script has. The documentation says to check it out first before trying to make the script work for you.

Why can I not use deduplication with zfs-inplace-rebalancing?

Because the working principle essentially creates a duplicate file on purpose, deduplication prevents it from working as intended. The documentation says you would have to resort to a more expensive rebalancing method that involves additional drives.

What free space does zfs-inplace-rebalancing need?

The process is not entirely in place, because a file has to be fully copied before the original is deleted, so you need enough space for a copy of the biggest file in your target directory. With a snapshot of the data, ZFS tracks the data twice and you effectively use double the file size of the target directory.

How does the script know which files it has already rebalanced?

It keeps a database file called rebalance_db.txt in its working directory with two lines per processed file, the path and the current pass count, and the highest count in an inode group determines whether that group is skipped for a given number of passes. The database can be disabled with --passes 0.

Official sources

  1. Issues
  2. License: CC0-1.0
  3. markusressel/zfs-inplace-rebalancing on GitHub
  4. README
Add this badge to your README

If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/markusressel-zfs-inplace-rebalancing.svg)](https://hysenlabs.com/projects/markusressel-zfs-inplace-rebalancing)

Was this useful?