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
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 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 gaveResult
free space, and it is at least the estimatepass
free space, and it is below the estimatefail — the finding names how many objects were measured, the estimate and the free space
a size but no free spaceundetermined, filesystem_headroom_unreadable, carrying the estimate
no statistics reader on this runundetermined — "the operation still needs it"
an error while reading the sizesundetermined, quoting what the server said
no object reported a size at allundetermined — "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 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_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 no object in scope reported a size the check answers undetermined rather 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 pass is 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.