DiskWatch
Capacity forecasting for directory trees
Techlosoft - Storage Health Center
WHAT IT IS
==========
DiskWatch answers one question the rest of the Storage Health Center line does
not: "at the rate this is filling up, WHEN do I run out?"
You take usage samples over time. DiskWatch stores each one as a timestamped
line in an append-only history file. When you ask for a forecast, it fits a
least-squares linear regression of bytes against time for every watched path
and projects the date the tree crosses a capacity you state.
It reports the growth slope, the R-squared goodness-of-fit so you can tell a
trustworthy trend from noise, the duration of the sample window, and the
projected days until full.
Sibling tools cover the other angles: DrivePulse maps folder space and finds
the largest files, StorageLens and SpaceMedic inspect and diagnose, DevicePulse
tracks snapshot history with growth-percentage warnings. DiskWatch is the only
one that predicts.
INSTALL
=======
Pre-built binaries are in dist/. There is nothing to install - copy the one for
your platform anywhere on your PATH and run it.
dist/diskwatch-linux-amd64 Linux, x86-64
dist/diskwatch-darwin-arm64 macOS, Apple Silicon
dist/diskwatch-darwin-amd64 macOS, Intel
dist/diskwatch-windows-amd64.exe Windows, x86-64
On macOS and Linux you may need to mark it executable:
chmod +x diskwatch-linux-amd64
To build from source you need Go 1.24 or newer. There are no dependencies of
any kind, so no network access is required:
go build -o diskwatch .
COMMANDS
========
diskwatch sample --watch
[--watch ...] --history
[--capacity 10GB] [--json]
diskwatch forecast --history [--capacity 10GB] [--json]
diskwatch history --history [--json]
diskwatch help | -h | --help
sample
Walks each watched tree, sums the apparent size of every regular file, and
appends exactly ONE JSON line to the history file. Existing lines are never
read back, rewritten or reordered - the history is strictly append-only, so
it is safe to run from cron while something else is reading it.
forecast
Groups the recorded samples by path, fits a least-squares line to each one
independently, and reports the trend plus the projected exhaustion date.
history
Lists the samples recorded so far, newest last.
FLAGS
--watch Directory tree to measure. Repeat the flag to watch
several trees in a single history. Bare positional
arguments to `sample` are also treated as watched dirs.
--history The JSON-lines history file. Created on first sample,
along with any missing parent directories.
--capacity The capacity to forecast against. If you omit it on
forecast, the most recent capacity recorded in the
history is used.
--json Machine-readable output (forecast and history).
Short forms -w, -f and -c are accepted for --watch, --history and --capacity.
Flags may appear before or after positional arguments; either order works.
CAPACITY STRINGS
================
10GB 10 GiB (10737418240 bytes)
500MB 500 MiB (524288000 bytes)
2TiB 2 TiB (2199023255552 bytes)
1.5G 1.5 GiB (1610612736 bytes)
1048576 raw bytes
Suffixes are BINARY: 1KB means 1024 bytes, not 1000. KB/MB/GB and KiB/MiB/GiB
are treated identically. A bare number is a byte count. Underscores and commas
in the number are ignored, so 1_000_000 and 1,000,000 both work.
QUICK START
===========
# take a sample now, and again later (cron, a timer, or by hand)
diskwatch sample --watch /var/log --history usage.jsonl --capacity 20GB
diskwatch sample --watch /var/log --history usage.jsonl --capacity 20GB
# once you have two or more samples
diskwatch forecast --history usage.jsonl
# what has been recorded so far
diskwatch history --history usage.jsonl
# feed a monitoring system
diskwatch forecast --history usage.jsonl --json
EXAMPLE OUTPUT
==============
DiskWatch capacity forecast
history : /srv/usage.jsonl (4 samples)
capacity : 20.0 GiB (21474836480 bytes, from history)
/var/log
samples : 4
window : 2026-08-03T02:00:00Z -> 2026-08-10T02:00:00Z (7d, 7.000000 days)
latest usage : 12.4 GiB (13314398618 bytes) [62.0% of capacity]
growth rate : 640.0 MiB/day (671088640.000 bytes/day)
fit R^2 : 0.998412
days to full : 12.160000 (12.16 days)
full at : 2026-08-22T05:50:24Z
status : projected - projected to reach 20.0 GiB in 12.16 days
HOW THE FORECAST IS COMPUTED
============================
For each watched path, every recorded sample becomes a point (x, y) where x is
days elapsed since that path's FIRST sample and y is the measured byte count.
DiskWatch fits the ordinary least-squares line y = intercept + slope * x:
slope = sum((x - mean_x) * (y - mean_y)) / sum((x - mean_x)^2)
intercept = mean_y - slope * mean_x
The slope IS the growth rate in bytes per day. Goodness of fit is the standard
coefficient of determination:
R^2 = 1 - sum((y - fitted_y)^2) / sum((y - mean_y)^2)
R^2 near 1.0 means the usage is tracking a straight line closely and the
projection is worth acting on. A low R^2 means the growth is erratic and the
projected date is a rough guess - collect more samples.
The projected exhaustion point solves the fitted line for the capacity:
x_full = (capacity - intercept) / slope
days until = x_full - x_of_last_sample
Note that the projection is anchored at the LAST SAMPLE, not at the moment you
happen to run forecast, so re-running forecast without sampling does not change
the answer.
A NOTE ON SHORT SAMPLE WINDOWS
Slope is always normalised to bytes PER DAY. If your samples are seconds apart,
a few megabytes of growth extrapolates to an enormous per-day figure - tens of
gigabytes per day is entirely normal for samples ten seconds apart. That is the
arithmetic working correctly, not a bug. This is why every forecast prints the
sample window duration next to the rate: a slope of 40 GiB/day measured over a
9-second window is a statement about 9 seconds, not a prediction about tomorrow.
For projections you intend to trust, sample over hours or days.
EDGE CASES, HANDLED EXPLICITLY
fewer than 2 samples Reports "need at least 2 samples to fit a trend".
Exits 0. No line is fitted and no number is invented.
zero or negative slope Reports "not growing - no exhaustion projected".
No projection, no division by a zero slope.
already over capacity Reports how far over the tree already is and tells
you to free space. Never prints a negative
days-until-full.
flat usage R^2 is undefined when the data has no variance
(total sum of squares is zero). It is printed as
"n/a" and emitted as null in JSON, not as a fake 1.0.
identical timestamps If every sample shares one timestamp there is no time
span to regress against; that is reported rather than
dividing by zero.
missing history file Tells you to run `sample` first and exits 1.
WHAT IS MEASURED - AND WHAT IS NOT
==================================
IMPLEMENTED, AND WORKING TODAY
- Recursive byte usage of any directory tree you point it at.
- Multiple independent watched paths inside a single history file. Each path
is regressed separately, so a growing tree and a flat tree in the same
history produce a projection for the growing one only.
- Append-only JSON-lines history: durable, greppable, trivially parsed by
anything, and never rewritten in place.
- Least-squares linear regression with R-squared, on real timestamps.
- Capacity exhaustion projection in days plus a calendar date.
- Human-readable capacity strings and human-readable byte formatting.
- --json output on forecast and history for monitoring integrations.
- Unreadable files and directories are counted and skipped, never fatal, so a
permission-denied subdirectory does not abort the sample.
DELIBERATE LIMITS OF THE CURRENT MEASUREMENT
- Sizes are APPARENT sizes (the file length as reported by the OS), summed
over regular files. This is not the same as blocks actually consumed on
disk. Sparse files therefore read LARGER than their real footprint, and
filesystem compression, block-size rounding and deduplication are not
accounted for.
- Symbolic links are not followed and their targets are not counted, so a
tree cannot be double-counted through a link loop.
- Hard links are counted once per path encountered, so a file with two links
inside the same tree is counted twice.
- Directory entries themselves contribute no bytes; only file contents do.
- Capacity is a number YOU state. DiskWatch does not discover it.
- The trend model is strictly linear. Genuinely exponential growth will be
under-projected, and a log-rotated tree that grows and drops in a sawtooth
is not modelled well by a straight line - watch R-squared.
NOT IMPLEMENTED - ROADMAP
These need privileged access or native per-OS APIs that a portable, dependency
free, standard-library-only binary cannot reach. They are honestly absent, not
stubbed:
- Real per-volume capacity and free space via statfs/statvfs (Unix) and
GetDiskFreeSpaceEx (Windows), so --capacity could default to the actual
size of the filesystem holding the watched path instead of being stated by
hand.
- True on-disk block usage (st_blocks) rather than apparent file size,
including correct sparse-file and compressed-file accounting.
- SMART attributes, drive temperature, reallocated-sector counts and
remaining-life estimates. These require raw device ioctls and root or
Administrator privileges.
- Background scheduling. DiskWatch never daemonises and never installs
itself. Run `sample` from cron, systemd timers, launchd or Task Scheduler;
the append-only history is designed exactly for that.
- Alerting: no email, webhook, Slack or exit-code threshold ("fail if fewer
than N days remain"). Parse --json and act on it yourself for now.
- Non-linear trend models (exponential, seasonal, sawtooth-aware) and
confidence intervals on the projected date.
- Network, cloud-bucket and remote filesystem targets.
- Per-file or per-subdirectory attribution of the growth. DiskWatch tells you
that a tree is growing and when it will fill; use DrivePulse to find out
which files inside it are responsible.
EXIT CODES
==========
0 Success. This includes forecasts that cannot project - "need at least
2 samples", "not growing" and "already over capacity" are legitimate
answers, not failures.
1 Bad invocation (unknown command, missing required flag, unparseable
capacity) or an I/O error (missing history file, unreadable watched
path, corrupt history line). Explicit help exits 0.
HISTORY FILE FORMAT
===================
One JSON object per line, UTF-8, newline-terminated, appended in chronological
order:
{"ts":"2026-08-10T03:38:45.843510443Z","capacity":10485760,
"entries":[{"path":"/tmp/data","bytes":2000000,"files":1,"dirs":0,"errors":0}]}
ts RFC 3339 UTC timestamp, nanosecond precision
capacity bytes, omitted when no --capacity was given
entries one object per watched path
path absolute path as resolved at sample time
bytes total apparent size of regular files in the tree
files number of regular files counted
dirs number of subdirectories below the root
errors entries skipped because they could not be read
The format is deliberately boring. You can append to it from your own scripts,
split it, archive it, or read it with one line of jq.