Why Startups Default to PostgreSQL, and Its Limits

why startups default to postgresql

PostgreSQL has become the answer that no engineering lead has to defend in a design review, which is a genuine change from a decade ago. The reason is not that it wins every benchmark. It is that a single Postgres instance covers relational data, JSON documents, full-text search, geospatial queries and vector similarity well enough that a seed-stage team can postpone four infrastructure decisions.

That default is defensible, but it comes with a maintenance bill that arrives later, usually in the form of autovacuum, connection exhaustion, or a major-version upgrade nobody scheduled.

The adoption evidence

The clearest published signal is Stack Overflow’s developer survey. In the 2025 edition, 55.6% of all respondents reported using PostgreSQL, against 40.5% for MySQL, 37.5% for SQLite and 30.1% for Microsoft SQL Server. Among professional developers the Postgres figure rises to 58.2%. Around 26,000 respondents answered the database question.

The trend matters more than the absolute number. In the 2024 survey, PostgreSQL was at 48.7% of all respondents and 51.9% of professionals, with MySQL at 40.3%. MySQL held roughly flat while Postgres added about seven percentage points in a year. Worth noting the counterweight: popularity indices built on job postings and search volume rather than self-reported use, such as the DB-Engines ranking, still scored Oracle, MySQL and SQL Server above PostgreSQL in the snapshot retrieved for this article, which tells you something about installed enterprise base versus new-project choice.

Extensions are the actual product

The extension system is what separates Postgres from a good relational database. Extensions install as first-class citizens with their own types, operators and index access methods, which means they get planner integration rather than bolt-on function calls.

PostGIS

PostGIS adds spatial types for points, lines, polygons and multi-geometries in 2D and 3D, spatial indexing, distance and intersection functions, raster handling, geocoding, and interoperability with QGIS, GeoServer, MapServer and ArcGIS. For any product with a map in it, this removes the need for a separate geospatial store.

pgvector

pgvector is why a lot of retrieval-augmented generation stacks never bought a dedicated vector database. It supports HNSW and IVFFlat indexes, six distance operators (L2, inner product, cosine, L1, Hamming and Jaccard), and four types: vector up to 2,000 dimensions, halfvec up to 4,000, bit up to 64,000, and sparsevec with up to 1,000 non-zero elements. It supports PostgreSQL 13 and later. The practical advantage is transactional: embeddings and the rows they describe commit together, and you can filter on ordinary SQL predicates in the same query as the similarity search.

JSONB

jsonb stores parsed JSON in a binary form, supports GIN indexing of keys and values, and lets you write containment and path queries in SQL. It is the pressure valve for schema churn: put the volatile fields in a jsonb column, keep the fields you filter and join on as real columns. The failure mode is using it as a substitute for schema design, at which point you inherit a document database with none of the tooling and all of the row width.

MVCC, and the vacuum bill

Postgres implements multiversion concurrency control by never modifying a row in place. An UPDATE writes a new row version and leaves the old one visible to transactions that still need it. Readers therefore never block writers, which is the property that makes Postgres pleasant under mixed load. The cost is that dead tuples accumulate and something has to reclaim them.

That something is VACUUM. The official documentation lists four jobs: recover disk space from dead rows, update planner statistics, maintain the visibility map that enables index-only scans, and prevent transaction ID wraparound. The last one is the one that pages you at 3am.

Transaction IDs are 32-bit. The space is four billion, and every table must be vacuumed at least once every two billion transactions or old rows begin to look like future rows. The default autovacuum_freeze_max_age is 200 million transactions, at which point a forced anti-wraparound vacuum kicks in regardless of your settings. Postgres starts warning 40 million transactions from wraparound and refuses to issue new transaction IDs at 3 million remaining.

Put numbers on that. A service running a steady 2,000 write transactions per second consumes 172.8 million transaction IDs a day. It hits the 200-million freeze threshold in a little over a day, and it would burn the entire two-billion safe budget in under 12 days if vacuum never ran. This is why aggressive autovacuum settings on high-churn tables are not an optimisation; on a busy system they are the operating condition, and per-table autovacuum_vacuum_scale_factor tuning is table stakes.

Replication choices

Mechanism Granularity Cross-version Main constraint
Physical streaming replication Whole cluster, byte-for-byte No Replica is read-only and identical; cannot filter tables
Logical replication Per table or publication Yes No DDL, no sequences, no large objects
WAL archiving with point-in-time recovery Whole cluster, restore to a timestamp No Recovery time scales with base backup age and WAL volume

Logical replication is the workhorse for migrations and near-zero-downtime upgrades, and its documented restrictions are worth reading before you plan one. Schema and DDL are not replicated, so the initial schema goes across with pg_dump --schema-only and subsequent changes are your problem. Sequence data is not replicated, so a failover leaves identity columns pointing at start values unless you bump them by hand. Large objects are not replicated at all, with no workaround. Views, materialised views and foreign tables cannot be replicated. And TRUNCATE propagation fails if truncated tables on the subscriber have foreign-key links to tables outside the subscription.

Where Postgres genuinely struggles

Connections. The documentation is explicit that the server “starts (‘forks’) a new process for each connection,” as described in the architectural fundamentals. A process per client is expensive, which is why any serverless or high-concurrency application front-end needs a pooler. PgBouncer costs about 2 kB per connection by default, but transaction pooling mode “breaks client expectations of the server by design“: no SET/RESET, no LISTEN, no WITH HOLD cursors, no protocol-level prepared statements without cooperation, no session advisory locks. Teams discover this when an ORM sets a session variable and it lands on a different backend.

Write amplification and bloat. Because updates write new tuples, every index on the table may also need updating. Heavily updated wide tables with many indexes are the classic Postgres performance cliff, and index bloat frequently needs REINDEX CONCURRENTLY rather than vacuum alone.

Analytics at scale. Postgres is a row store with a single-threaded-per-query heritage; parallel query helps but it is not a columnar warehouse. Large scans over billions of rows belong somewhere else, or in a columnar extension.

Multi-primary writes. There is no built-in active-active. Horizontal write scaling means sharding at the application layer or adopting a distributed fork, and both are architectural commitments rather than configuration changes.

Upgrade cadence. As of the August 2026 release round, the supported line runs from 14 through 18.6, with PostgreSQL 19 in beta. The project’s release announcements state that PostgreSQL 14 stops receiving fixes on 12 November 2026. Five years of support is generous, and it still means a mandatory upgrade project roughly every five years for every cluster you own.

What to do before you outgrow the default

Choosing Postgres is the easy part. Three things are worth doing while the database is still small: put a pooler in the path before your connection count forces you to, set per-table autovacuum thresholds on your highest-churn tables rather than relying on cluster defaults, and rehearse a major-version upgrade on a copy of production once, so the runbook exists before the end-of-life date does. The teams that regret Postgres are almost never the ones that picked it. They are the ones that treated it as a service someone else operates.

Sources

Post Comment