Contents
On September 24, Matthias van de Meent filed a bug report on pgsql-hackers with a title that says most of what you need to know: pg_class.relchecks overflow, making table undroppable.[1] relchecks is the column in Postgres’s system catalog that records how many CHECK constraints a given table has. It’s stored as a smallint, a 16-bit signed integer with a ceiling of 32,767. Create more than that many CHECK constraints on one table, and the counter wraps. Michael Paquier committed the fix on September 27 (September 28 in his local timezone), backpatched through Postgres 14, the oldest branch that still receives fixes as of this year.[2] The bug had existed, unnoticed, in every Postgres release for as long as pg_class.relchecks has been a plain counter, which is effectively forever.
What actually breaks
Two things go wrong once the counter overflows, and neither is subtle. First, anything that loads the relation into the relation cache starts emitting warnings, because the cached constraint count no longer matches reality. Second, and worse, the table becomes undroppable. Dropping a CHECK constraint decrements relchecks, and Postgres has an assertion-level guard that refuses to let the counter go negative, raising an error instead. Once the smallint has wrapped past its positive range, every attempt to drop a constraint on that table hits the guard and fails. You can’t get the count back down, because getting it down is exactly the operation the guard blocks. The table is stuck with whatever constraints it had at the moment of overflow, and you can’t clean any of them off it. DROP TABLE itself may still work, since it doesn’t go through the same per-constraint accounting path, but incremental cleanup does not.
The reporter also noted that the code had two live proposals sitting in the codebase for years: enforce a hard ceiling on the constraint count, or get rid of relchecks as a live counter entirely and replace it with a boolean haschecks flag, since a boolean is all ANALYZE and the planner actually need to know whether a table has any check constraints at all. The committed fix took the first option. A boolean would have been the more elegant long-term answer, but relchecks is read directly by tools and by \d in psql as a literal count, so replacing it changes an externally visible catalog column’s semantics for anyone scripting against pg_class. Capping the counter at its natural maximum was the change that didn’t require touching that contract.
Who actually reaches 32,767
Nobody writes CREATE TABLE with 32,767 CHECK clauses by hand. The realistic path here is generated DDL: a migration framework or code generator that emits one CHECK constraint per allowed value for a column, instead of a native ENUM type or a lookup table, run inside a loop with no bound on iteration count. It’s also a shape that shows up in multi-tenant systems where an application role, not a DBA, has ALTER TABLE privileges on its own schema and drives constraint creation from user-controlled input, a list of categories, a set of validation rules, a per-customer allow-list. None of that requires superuser access or unusual privileges. Any role that can run ALTER TABLE ... ADD CONSTRAINT in a loop, deliberately or through a bug in its own code, could wedge a table into a permanently undroppable state under every affected version. That makes it a plain robustness bug rather than a security boundary, but it’s the kind of self-inflicted denial of service that’s worse than a crash: a crash restarts, an undroppable table sits there.
The part worth remembering after this specific bug is patched
The patch that landed isn’t just “add a check for > 32767.” The review thread, which ran three revisions in the space of a day between Bertrand Drouvot, Jian He, and the original reporter, spent most of its effort on where the check goes.[1] An early version compared the incremented count against the limit after storing the new constraint, which is the natural place to put a check if you’re reading the code top to bottom. Reviewers pushed it earlier: use pg_add_s16_overflow(), Postgres’s overflow-safe addition helper, to detect the overflow before the constraint is written to pg_constraint and before relchecks is updated in pg_class, and raise ERRCODE_PROGRAM_LIMIT_EXCEEDED with a clear “too many check constraints on relation” message at that point instead. They also caught an off-by-one: the first version rejected the constraint at exactly 32,767, when the documented and intended limit is that 32,767 should still be legal, since it’s the exact maximum a smallint can represent. The fix that shipped changes the comparison from >= to >.[2]
The reason the check has to happen before the write, not after, is the same reason the undroppable-table bug existed at all. Once an invalid state is on disk in a system catalog, undoing it means running the exact operations that got you into the state in the first place, and those operations are the ones that are now broken. A cleanup pass that runs after the corrupted counter is already committed has nothing consistent to clean up from. Validating at the point of insertion doesn’t just avoid a bug, it avoids ever putting the system into a state that requires cleanup logic to exist. That’s a more general lesson than this one column, and it shows up anywhere a system tracks a running count that something else assumes can never go negative: sequence-backed IDs, reference counts, quota decrements. The fix here is small. The reason it took three review rounds to land in the right place is not.
Anyone running schema-generation code that creates CHECK constraints programmatically, rather than by hand, has a concrete reason to check for an upper bound today. The fix documents the limit explicitly in limits.sgml for the first time, but it will only reach your database once the next scheduled minor release for your major version ships.[2] Until then, “far more than a few thousand CHECK constraints on a single table” was never a supported shape for a Postgres table, even though nothing enforced that until this week.
Sources
[1] https://github.com/MisterRaindrop/postgresql-github-mirror/pull/592: “BUG: pg_class.relchecks overflow, making table undroppable,” mirrored pgsql-hackers thread, September 24-27, 2026
[2] https://github.com/postgres/postgres/commit/5e59290: “Enforce pg_class.relchecks limit (32,767 or INT16_MAX),” commit by Michael Paquier, authored by Matthias van de Meent, September 27-28, 2026, backpatched through PostgreSQL 14