DEPLOY.PREFLIGHT.DISK_HEADROOM — How much space the pending migration will need, against what the instance can see
- Category: safety
- Severity: high
- Level: 0
- Downtime class:
rewrite— the finding this check emits when it fails carries it, because what it describes is a rewrite about to run - Stability: stable
- Suites: deploy
- Applies to: PostgreSQL and MySQL
What it reports
The check reads the pending migrations the run already canonicalized — the same ones sqlens:lint
sees — and picks out the statement kinds that need space again while they run:
| Statement kind | Why it is counted |
|---|---|
alter_table | a rewrite keeps the old copy readable until the new one is complete, so the peak is roughly double |
alter_column | as above |
add_primary_key | as above |
create_index | needs the index rather than the table — smaller, and still real |
Everything else is passed over. A drop_column frees space rather than needing it, and a migration
whose statements are all of other kinds passes without a word.
For the statements that are counted, the check asks the run's statistics reader how big each object
they target is, and sums the total size the reading reported for each one — every object counted
once, however many statements name it. Nothing is queried from the tables themselves: on PostgreSQL
those numbers come from pg_total_relation_size over pg_class (summed across
pg_partition_tree() for a partitioned parent, which has no storage of its own and would otherwise
report zero), and on MySQL from DATA_LENGTH + INDEX_LENGTH in information_schema.TABLES.
Then the second half of the question: the reading's storage-headroom entries are searched for a free-space number. What comes out is one of six answers.
| What the reading gave | Result |
|---|---|
| free space, and it is at least the estimate | pass |
| free space, and it is below the estimate | fail — the finding names how many objects were measured, the estimate and the free space |
| a size but no free space | undetermined, filesystem_headroom_unreadable, carrying the estimate |
| no statistics reader on this run | undetermined — "the operation still needs it" |
| an error while reading the sizes | undetermined, quoting what the server said |
| no object reported a size at all | undetermined — "an unmeasured table is not a small one" |
The answer you will usually get
Free filesystem space is usually not readable from inside the database. A managed instance does not expose it at all, no amount of privilege changes that, and this package will not shell out to a database server to learn it. Both shipped readers therefore report the size they can establish and a named absence for the free half:
- PostgreSQL has no SQL for the free space on the volume its data directory sits on.
pg_database_size(current_database())says how much the database occupies; nothing says how much is left. - MySQL has no more than that.
DATA_FREElooks like the answer and is not — it is reclaimable room inside a tablespace file, fragmentation rather than capacity, and a gate told "there is room" on the strength of it would wave through the rewrite that fills the disk.
So on most instances the result is undetermined with the reason filesystem_headroom_unreadable,
and it carries the estimate anyway: how many of the objects the pending rewrite touches came back
with a size, roughly how much those total (rounded to GiB, MiB or KiB), and why a rewrite needs that
much again.
Withholding the number there would make the verdict correct and useless. It is the one thing a human in a deploy window can hold against their own monitoring.
Why it matters
A table rewrite needs the table's size again while it runs, because the old copy stays readable until the new one is complete. An index build needs its own space. An instance that fills up mid-rewrite stops — which is the most expensive way a deploy can end.
What to do about it
On a fail, the finding has already done the arithmetic: this many objects, about this much
needed, about this much free. The decision is whether to make room or to not run this migration now,
and it is a decision rather than a fix:
- Reclaim what is holding space. On PostgreSQL,
DEPLOY.PREFLIGHT.INACTIVE_REPLICATION_SLOTruns immediately before this check for exactly that reason — a slot whose consumer is gone goes on holding WAL segments, unbounded unlessmax_slot_wal_keep_sizeis set, and it is the commonest cause of a full disk that the instance can actually name. MySQL has no counterpart to run there: binary-log retention is a time-or-size policy on the primary rather than a per-consumer reservation. - Or grow the volume or the quota before the migration runs, and re-run the gate.
On the undetermined, the estimate is yours to check, because the database cannot. Hold the
number against your own monitoring or your provider's storage graph. If you have satisfied yourself
there is room, --allow-undetermined proceeds — it opens exactly one door, a run whose blockers are
only undetermined, and a real failure still stops the deploy. It is a flag rather than a default so
that using it is a decision somebody made.
What the reading costs the server
Catalog and statistics views only, in the session the reader layer already sealed read-only, taking
no locks of its own. On MySQL the read is served from cached statistics: the reader does not touch
information_schema_stats_expiry and sends no ANALYZE TABLE, because forcing a fresh read of every
table's statistics is exactly the expensive behavior a gate must not trigger immediately before a
deploy.
What it does not claim
- It is an estimate, never a measurement. The need is derived from current object sizes, and what
a rewrite actually writes depends on fill factor, TOAST, index rebuilds and the WAL the operation
generates. The finding carries
confidence: heuristicas well as saying so in its text, so a reader filtering on confidence can tell it from something the catalog stated. - It does not look at your filesystem. It reads only what the instance reports about its own storage. On a managed platform that figure can be a quota rather than a disk, and an autoscaling volume makes it a moving target.
- A missing size is never invented. Treating an object the reading could not measure as empty
would report the largest possible rewrite as the safest. When no object in scope reported a size
the check answers
undeterminedrather than guess at one. When some reported a size and others did not, the estimate covers only the ones that were measured — that is what the object count in the message means, and it is why that count can be smaller than the number of objects the migration names. An estimate built from part of the set is a floor, not a total. - A
passis narrow. It means either that none of the pending statements is one of the four kinds weighed here, or that the free space the instance reported was at least the estimate. It is not a statement about how long the migration takes or what it locks.