Skip to main content

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 kindWhy it is counted
alter_tablea rewrite keeps the old copy readable until the new one is complete, so the peak is roughly double
add_columnas above, whenever the server materializes the new value: a volatile default on PostgreSQL, ALGORITHM=COPY on MySQL
alter_columnas above
add_primary_keyas above
create_indexneeds 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 table they target is, and sums the total size the reading reported for each one — every table counted once, however many statements name it. An index build is weighed by the table it reads: the index it names does not exist before the deploy, so there is nothing to measure. 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.

A table the same batch creates with a column list is the one object that needs no size: it holds no rows when the rewrite reaches it, and a reader that has never seen it has nothing to say about it. A table created by CREATE TABLE … AS SELECT holds the rows its query returns, so it is weighed like any other.

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 seven answers.

What the reading gaveResult
free space, and it is at least the estimatepass
free space, and it is below the estimatefail — the finding names how many objects the batch touches, the estimate and the free space
a size for every object but no free spaceundetermined, filesystem_headroom_unreadable, carrying the estimate
every object new and empty, or measured at zeropass — there is nothing to hold room for
no statistics reader on this runundetermined — "the operation still needs it"
an error while reading the sizesundetermined, quoting what the server said
no size for one object or moreundetermined, object_statistics_unread, naming each one and what the measured ones come to — "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_FREE looks 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 objects the pending rewrite touches, roughly how much they 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_SLOT runs immediately before this check for exactly that reason — a slot whose consumer is gone goes on holding WAL segments, unbounded unless max_slot_wal_keep_size is 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: heuristic as 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 any object in scope reported no size, the check answers undetermined and names each one, with what the measured ones come to: an estimate built from part of the set is a floor, and comparing a floor with the free space would let the smallest table decide for the largest. The one exception is a table the same batch creates with a column list, which is empty by construction.
  • A pass is narrow. It means that none of the pending statements is one of the five kinds weighed here, that every object they rewrite is new and empty, 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.