The case against unbounded text
Postgres novices quickly learn a pleasant truth: there is no performance penalty for using text over varchar(n). The documentation is explicit that the blank-padded character(n) is usually the slowest of the three, and that length checks add only a few CPU cycles when storing into a constrained column. For developers coming from MySQL or Oracle, where choosing a length for every string column is a reflex, switching to unbounded text feels liberating.
That freedom, however, has a hidden cost that only shows up at scale. The experience at Stripe around 2018 illustrates why.
When nothing is bounded
At Stripe, the API framework technically supported maximum lengths for text parameters, but no sensible default had ever been assigned. As a result, practically none of the thousands of API parameters enforced any limit. As long as a request fit within the payload size, a client could send arbitrarily large strings in any field, and the API would store them in Mongo without complaint.
The problem surfaced when certain users sent enormous payloads that crashed HTTP workers and strained database resources. The fix sounded straightforward—add length validation—but the reality was more complicated. The API already had countless users, and with enough traffic, someone will always exploit the absence of a constraint. Hundreds or thousands of legitimate users were sending huge text payloads as part of their normal workflows: whole product catalogs, large JSON blobs, and other unconventional but valid integration patterns.
Introducing hard limits would have broken those integrations. Stripe takes backward compatibility seriously, and active outreach to change user behavior on this scale was not practical. The compromise was a liberal limit of 5000 characters (still visible in the public OpenAPI spec), with exemptions for the few users who exceeded even that.
The episode is a textbook case of Hyrum's law:
With a sufficient number of users of an API, it does not matter what you promise in the contract: all observable behaviors of your system will be depended on by somebody.
Returning to varchar, with tiers
The lesson generalizes beyond APIs. When a new service starts writing strings directly to a database, it is easy to forget length checks entirely—again. The Postgres wisdom of "just use text" is generally sound, but it has edge-case ramifications that are hard to see until they bite.
The remedy is to return to varchar, but without micromanaging arbitrary lengths per column. A practical approach is to adopt a small set of order-of-magnitude tiers:
varchar(200)for short strings: names, addresses, email addresses.varchar(2000)for longer text blocks like descriptions.varchar(20000)for very long text blocks.
The numbers should be liberal enough that no legitimate input ever hits the ceiling. The constraint is a backstop against wildly incorrect data, not a prediction of typical values. These specific figures are a starting point, not a prescription; the right tiers depend on the application.
Database constraints do not replace validation in application code. Most programs are not built to handle constraint violations gracefully, so a standard error-handling layer with clear messages is still necessary. The database is the last line of defense, not the first.
Relaxing constraints without pain
One historical argument against varchar was operational: changing a column type could require a full table rewrite, putting a hot database at risk. That concern has largely evaporated for the common case of lengthening a column. As the Postgres ALTER TABLE documentation notes, a table rewrite is avoided when the USING clause does not change the column contents and the old type is binary coercible to the new type—or is an unconstrained domain over it.
A varchar(200) is an unconstrained domain over a varchar(100) because it is strictly longer. So Postgres can relax the length limit without locking the table for a scan. Shrinking a column is still expensive, but that operation should rarely be needed.
Domains for consistency
Another option is to encode the tier system as SQL domains. A domain defines a new type with constraints on top of a base type, and can be used in table definitions to enforce the same limits by convention rather than by copying the same varchar(n) everywhere.
CREATE DOMAIN text_standard AS varchar(200) COLLATE "C";
CREATE DOMAIN text_long AS varchar(2000) COLLATE "C";
CREATE DOMAIN text_huge AS varchar(20000) COLLATE "C";
# CREATE TABLE mytext (standard text_standard, long text_long, huge text_huge);
# \d+ mytext
Table "public.mytext"
Column | Type | Collation | Nullable | Default | Storage | Stats target | Description
----------+---------------+-----------+----------+---------+----------+--------------+-------------
standard | text_standard | | | | extended | |
long | text_long | | | | extended | |
huge | text_huge | | | | extended | |
The downside is discoverability. Column types shown in \d output will be domain names, not familiar base types. Postgres can reveal the underlying definitions with \dD, but few developers will know to do that off the top of their head:
# \dD
List of domains
Schema | Name | Type | Collation | Nullable | Default | Check
--------+---------------+--------------------------+-----------+----------+---------+-------
public | text_huge | character varying(20000) | C | | |
public | text_long | character varying(2000) | C | | |
public | text_standard | character varying(200) | C | | |
Constraints as integrity
Length limits on text fields are a small piece of a larger philosophy. Relational databases enforce data integrity through types, foreign keys, check constraints, ACID, and triggers. That pedantry can feel rigid early on, but it is what lets you trust the data in your system. When constraints exist, you do not have to wonder whether a column holds a valid value—you know it does.



