Most PostgreSQL vs MySQL articles are feature lists scraped from documentation. Both support ACID transactions, both have replication, both are fast. None of it helps at 2am when a migration has locked your users table and your SaaS is down.
This guide covers what breaks, what costs money, and what slows a team down eighteen months in. We build production backends for startups and agencies on both engines.
The short answer, and the at a glance comparison
For a new SaaS in 2026, default to PostgreSQL. Not because MySQL is bad, but because PostgreSQL gives you more optionality for the same operational cost, and optionality is what early SaaS products need most.
You do not yet know whether you will need vector search, geospatial queries, JSON documents alongside relational rows, or row level security. PostgreSQL adds all of those without a second datastore. MySQL means bolting on Elasticsearch or a vector database sooner.
Choose MySQL when you have a specific, named reason. Those reasons exist and we cover them later.
| Dimension | PostgreSQL | MySQL (InnoDB) | | --- | --- | --- | | Default choice for new SaaS | Yes | Only with a named reason | | JSON support | JSONB, binary, fully indexable with GIN | JSON type, functional indexes only | | Full text search | Built in, good to roughly 5M documents | Weaker, usually needs Elasticsearch | | Vector / AI search | pgvector, production ready | External vector DB required | | Geospatial | PostGIS, best in class | Basic spatial types | | Query planner | Sophisticated, more plan types | Simpler, more predictable, fewer options | | Online schema change | CREATE INDEX CONCURRENTLY, needs care on ALTER | Strong online DDL, gh-ost, pt-osc ecosystem | | Replication | Streaming plus logical, solid | Very mature, huge operational knowledge base | | Connection cost | Heavy, process per connection, pooler required | Lighter, thread per connection | | Read heavy scale out | Good | Excellent, decades of tuning | | Managed hosting floor | Roughly 15 to 25 USD per month | Roughly 15 to 25 USD per month | | Extension ecosystem | Very large | Small | | Talent availability | High and rising | High, especially in older teams |
Where the two genuinely differ in 2026
Both engines have converged on the basics. MySQL 8.x has window functions, CTEs and real JSON. PostgreSQL 17 and 18 closed the historical gaps on replication and connection overhead. The old arguments are dead. What remains is philosophy, visible in three places.
PostgreSQL is a platform. Its extension system lets the database absorb workloads that would otherwise need separate infrastructure, so a two person team can run search, geospatial, vector similarity and relational data on one engine. Fewer moving parts means fewer failure modes, which matters more than throughput pre product market fit.
MySQL is a very good relational engine with an exceptional track record at read heavy scale. Boring in the best sense. High volume, simple queries, mostly reads, and someone who has run it before: it will not let you down.
Third is correctness by default. PostgreSQL is stricter and rejects data MySQL quietly coerces. Early on that feels like friction. Two years in it is why your reporting numbers still add up. We have inherited MySQL codebases where silent truncation and zero dates corrupted analytics for a year before anyone noticed.
If you are still choosing between relational and document storage, start with choosing between MongoDB and PostgreSQL for a SaaS. That decision comes before this one.
JSON and semi-structured data
Every SaaS ends up with semi-structured data: feature flags, per tenant settings, webhook payloads, form builder output, audit metadata. How the database handles that determines whether you need a second datastore.
PostgreSQL has JSONB, a binary parsed indexable representation. Index the whole document with GIN, or one extracted path with a B-tree.
-- index every key and value in the document
CREATE INDEX idx_events_payload ON events USING GIN (payload jsonb_path_ops);
-- index one hot path only, much smaller and faster
CREATE INDEX idx_events_customer
ON events ((payload->>'customer_id'));That second index turns a sequential scan over 10M rows into a sub-millisecond lookup. You can also constrain a JSON path, or promote a JSON field to a real column later without rewriting the application layer if access sits behind a repository.
MySQL's JSON type is functional but shallower. You cannot index a JSON column directly; you create a generated column and index that.
ALTER TABLE events
ADD COLUMN customer_id VARCHAR(64)
GENERATED ALWAYS AS (payload->>'$.customer_id') STORED,
ADD INDEX idx_events_customer (customer_id);It works. It is more ceremony, it is a schema change on a large table, and there is no equivalent of a GIN index for arbitrary key lookups. If your product has genuinely dynamic per tenant fields, PostgreSQL saves weeks over the life of the product.
The rule we apply when designing scalable Node.js backend architecture: relational columns for anything you filter, sort or join on; JSONB for anything you only read as a blob. Revisit that boundary quarterly.
Indexing and query planner behaviour
This is where most SaaS performance problems live, and where the engines feel most different in production.
PostgreSQL has richer index types: B-tree, GIN, GiST, BRIN, hash and partial. Partial indexes are underrated and save enormous space in SaaS schemas, where most queries only touch non-deleted, non-archived rows.
CREATE INDEX idx_active_subs ON subscriptions (tenant_id, renews_at)
WHERE status = 'active' AND deleted_at IS NULL;On a 10M row subscriptions table where 8% of rows are active, that index is roughly a twelfth the size of the full one. It stays in memory, so queries stay fast.
MySQL answers with the clustered primary key, which is genuinely elegant: secondary indexes carry the primary key, so a well chosen key gives you locality for free. If your access pattern is always "everything for one tenant, ordered by time", you get that layout without extra work.
The planners differ in temperament. PostgreSQL's is more capable and willing to pick an exotic plan; with stale statistics it can pick a spectacularly bad one. MySQL's is simpler: fewer wins, fewer disasters.
Two habits fix most of this. Run ANALYZE after any bulk load, because a 2M row import leaves statistics far behind reality. And read EXPLAIN (ANALYZE, BUFFERS) on every query touching more than 100k rows before it ships. We make that part of code review, alongside our AI assisted code review process. A plan reading 400k buffers to return 20 rows is a bug.
If your API is already slow under load, the cause is usually here rather than in application code. We covered the diagnostic sequence in why REST APIs in Node fall over under load.
Extensions: PostGIS, pgvector and full text search
This is the single largest practical gap in 2026, and it is why we default to PostgreSQL.
pgvector. Any AI feature, retrieval augmented generation, semantic search, deduplication or recommendations, needs vector similarity. pgvector is one extension and one column. No second database, no sync pipeline, no consistency problem between relational data and embeddings.
CREATE EXTENSION IF NOT EXISTS vector;
ALTER TABLE documents ADD COLUMN embedding vector(1536);
CREATE INDEX ON documents
USING hnsw (embedding vector_cosine_ops);An HNSW index over 1M embeddings returns top 10 nearest neighbours in roughly 5 to 20ms on a 4 vCPU instance. You only need a dedicated vector database above roughly 50M vectors, or with very high write throughput on embeddings. Most SaaS products never get there. This matters more every quarter as AI features become table stakes, something we see across every AI powered development engagement.
Full text search. tsvector with a GIN index handles a few million documents well. Add pg_trgm for fuzzy matching. You will not match Elasticsearch on relevance tuning or faceting, but you avoid a second cluster for at least two years.
PostGIS. For radius search, delivery zones or service areas, PostGIS is not merely better than MySQL's spatial support. It is a different category of tool.
MySQL has none of this. Each feature means a second system: another deployment, backup policy, set of credentials and consistency boundary. For a small team that is a real tax, and it widens the surface described in how to secure a Node.js API.
Replication and high availability
MySQL wins on accumulated operational knowledge, and it is not close: twenty years of war stories, mature tooling like Orchestrator, and a large pool of engineers who have run failover at 3am.
PostgreSQL's streaming replication is reliable, but failover historically meant assembling Patroni, repmgr or pg_auto_failover yourself. In 2026 that is largely solved if you use a managed provider, which you should.
| Capability | PostgreSQL | MySQL | | --- | --- | --- | | Physical replication | Streaming, byte level, very fast | Row based binlog replication | | Logical replication | Native since 10, solid in 16+ | Native, very mature | | Typical replica lag, healthy | Under 100ms | Under 100ms | | Automated failover, self hosted | Patroni or pg_auto_failover | Orchestrator or Group Replication | | Automated failover, managed | Handled by provider, 30 to 120s | Handled by provider, 30 to 120s | | Multi primary | Not native, use extensions | Group Replication, InnoDB Cluster | | Read replica routing | Application or proxy level | Application or ProxySQL |
Honest position: under roughly 5,000 concurrent users this section should not decide your choice. Use a managed provider, enable a standby, test your restore quarterly, move on. Failover mechanics only differentiate at a scale most products never reach.
What matters early is restore time. Test it. Teams discover mid incident that their 200GB restore takes four hours. That number belongs in the runbook before you need it.
Migrations and schema change under load
This is the most underrated criterion, and the one that causes real outages in growing SaaS products.
MySQL has better online DDL out of the box and a stronger external tooling ecosystem. gh-ost and pt-online-schema-change are battle tested for rebuilding huge tables with minimal locking, and many MySQL 8 ALTER TABLE operations run online with concurrent DML allowed.
PostgreSQL is faster for common cases but has sharper edges. Adding a nullable column is instant, and adding a column with a default has been instant since version 11. Creating an index requires care:
-- takes an ACCESS EXCLUSIVE lock, blocks writes, do not do this
CREATE INDEX idx_orders_tenant ON orders (tenant_id);
-- no write lock, takes longer, safe on live tables
CREATE INDEX CONCURRENTLY idx_orders_tenant ON orders (tenant_id);The trap is lock queuing. A PostgreSQL migration waiting for a lock queues behind a long running query, and every subsequent query queues behind the migration. One slow analytics query can take your application down during a migration that should have taken 50ms. The fix is a lock timeout on every migration:
SET lock_timeout = '3s';
SET statement_timeout = '30s';
ALTER TABLE orders ADD COLUMN currency TEXT;If the lock is not available in three seconds the migration fails cleanly and retries instead of taking down your product. Every PostgreSQL migration we ship carries these settings by default. It is a two line change that has prevented more incidents than any amount of clever indexing.
Whichever engine you choose, migrations should be expand and contract: add the column, backfill in batches, dual write, switch reads, then drop the old column in a later release. Never in one deploy. Same discipline we apply to API structure in Next.js applications, where a breaking contract change and a breaking schema change cause identical pain.
Connection handling and pooling in Node.js
PostgreSQL forks a process per connection, each costing roughly 5 to 10MB before it does any work. A managed instance might cap at 100 connections. Three Node.js instances with a pool of 20 each, plus a background worker and your migration runner, and you are at the ceiling.
MySQL uses threads, which are cheaper, and handles a few hundred connections more gracefully. That is a real advantage in serverless and autoscaling environments.
The fix for PostgreSQL is a pooler, and it is not optional in production: PgBouncer in transaction mode, or your provider's built in pooler.
// pg Pool sized for the instance, not for optimism
import { Pool } from 'pg';
export const pool = new Pool({
connectionString: process.env.DATABASE_URL,
max: 10, // per Node process, not per app
idleTimeoutMillis: 30_000,
connectionTimeoutMillis: 5_000,
statement_timeout: 15_000,
});Sizing rule: total pool size across all processes stays under 60% of the instance connection limit, leaving headroom for migrations, monitoring and manual access. Four containers at max: 10 means 40 connections against a limit of 100.
In serverless contexts, a normal pool does not work because each invocation is a fresh process. Use a pooler in transaction mode or an HTTP driver; Neon and Supabase ship both. Get this wrong and you exhaust connections under a spike, which surfaces as 500 errors that only appear in production.
One caveat: transaction mode pooling disables prepared statements and session features like SET LOCAL outside a transaction. Check your ORM first. Prisma, Drizzle and Knex all document configurations for this.
If you are also choosing the runtime, Node.js vs Python vs Go for backends covers how each connection model interacts with these limits.
Managed hosting and what you actually pay each month
Self hosting for a new SaaS is almost always wrong: engineering hours cost more than the service. Realistic 2026 monthly figures for a production SaaS with one primary and one standby.
| Provider | Engine | Small (2 vCPU, 8GB) | Mid (4 vCPU, 16GB) | Notes | | --- | --- | --- | --- | --- | | Neon | PostgreSQL | 19 to 40 USD | 69 to 200 USD | Scale to zero, branching, strong DX | | Supabase | PostgreSQL | 25 to 60 USD | 110 to 300 USD | Auth and storage bundled | | Render | PostgreSQL | 20 to 50 USD | 95 to 250 USD | Simple, good for small teams | | DigitalOcean | Both | 60 to 90 USD | 120 to 240 USD | HA node roughly doubles cost | | AWS RDS | Both | 70 to 120 USD | 190 to 400 USD | Multi AZ roughly doubles compute | | AWS Aurora | Both | 90 to 180 USD | 250 to 550 USD | Serverless v2 from about 45 USD idle | | Google Cloud SQL | Both | 80 to 140 USD | 200 to 420 USD | Similar shape to RDS | | PlanetScale | MySQL | 39 to 90 USD | 150 to 400 USD | Best in class branching for MySQL |
Costs are effectively identical between engines at the same instance size. Anyone claiming MySQL is cheaper is comparing different hardware.
Costs diverge on the second system. If MySQL pushes you to add Elasticsearch (from roughly 95 USD per month) and a vector database (roughly 70 USD per month), your bill is 165 USD higher and you have three failure domains instead of one. That is the real comparison.
For a pre revenue SaaS: start on Neon or Supabase at roughly 25 USD per month, budget 150 to 300 USD once you have paying customers, and consider RDS or Aurora only when compliance or an existing AWS commitment requires it. Those figures feed our MVP cost breakdown.
Multi-tenancy patterns
Your multi-tenancy model shapes the database choice more than any benchmark. Three patterns are viable.
Shared schema with a tenant_id column. Every table carries tenant_id, every query filters on it. Simplest to operate, cheapest to run, scales to thousands of tenants. The risk is a missing WHERE clause leaking data across tenants.
PostgreSQL has a genuine advantage here through Row Level Security, which enforces isolation in the database rather than trusting every developer to remember a filter.
ALTER TABLE invoices ENABLE ROW LEVEL SECURITY;
CREATE POLICY tenant_isolation ON invoices
USING (tenant_id = current_setting('app.tenant_id')::uuid);Your application sets app.tenant_id at the start of each request. A forgotten filter now returns zero rows instead of another customer's invoices. MySQL has no equivalent, so isolation lives entirely in application code and review.
Schema per tenant. PostgreSQL handles this to a few hundred tenants. Beyond that, migrations slow and the system catalog bloats. MySQL databases have the same ceiling.
Database per tenant. Necessary for enterprise data residency requirements. Both engines work; operational complexity dominates.
| Pattern | Tenant ceiling | Isolation strength | Migration effort | | --- | --- | --- | --- | | Shared schema plus tenant_id | 100,000+ | Medium, strong with RLS | Low, one migration | | Schema per tenant | 200 to 500 | High | High, N migrations | | Database per tenant | 50 to 200 | Very high | Very high |
Start with shared schema plus Row Level Security. Add database per tenant only when an enterprise contract requires it, and charge for it. Building isolation you do not yet need is the premature work we argue against in which MVP features to cut.
Performance at 1k, 100k and 10M rows
Benchmarks are mostly theatre. Here is what actually changes as data grows, for typical SaaS query shapes on a 4 vCPU, 16GB managed instance.
Up to 1,000 rows per table. Nothing matters. Both engines return everything in under 5ms with or without indexes. Do not optimise. Ship. Delivery speed is worth more than milliseconds, which is the argument in how fast MVP development actually happens.
Around 100,000 rows. Missing indexes start to hurt. A sequential scan on 100k rows costs roughly 30 to 80ms, and users notice at about 200ms total response time. Add indexes on every foreign key and every filtered column. Both engines behave almost identically. This is where N+1 queries in your data fetching layer become the dominant cost, not the database, which is why Next.js data fetching strategy matters as much as schema design.
Around 10 million rows. Real differences appear. Index selection, planner quality and physical layout decide whether a query takes 8ms or 4 seconds. PostgreSQL wins on partial indexes, BRIN for time series columns, and native declarative partitioning. MySQL wins on clustered primary key locality for tenant scoped range queries and on well trodden read replica scale out.
Figures we see in production on well indexed schemas at 10M rows: primary key lookup 0.3 to 1ms on both. Tenant scoped range query returning 50 rows 2 to 8ms on both. Aggregation over 1M rows 200 to 900ms on PostgreSQL with a parallel plan, 400ms to 2s on MySQL. Full text search over 2M documents 20 to 60ms on PostgreSQL, not practical on MySQL without Elasticsearch.
Above 100 million rows. You need partitioning, archival and probably a separate analytical store regardless of engine. Discipline matters more than the original choice. If you push heavy aggregates through an API at this size, read building GraphQL APIs that perform in production first.
The cost of switching later, and when MySQL is genuinely the better call
Switching databases at 10M rows is a three to six week project for a mid sized SaaS. Not because moving data is hard, but because everything around it needs rewriting.
Breakdown for a product with roughly 60 tables, one Node.js API and a small analytics layer: schema translation, three to five days. Data migration with dual write and verification, five to eight days. Query rewriting for engine specific SQL, five to ten days. ORM and migration tooling, two to four days. Regression and load testing, five days. Cutover with rollback, two days. Total: 22 to 34 engineering days, plus the feature work you did not do. At a blended senior rate that is 25,000 to 55,000 USD, plus cutover risk.
Four situations where MySQL is the right answer and we will recommend it.
Your team already runs MySQL well. Operational familiarity beats theoretical superiority every time. A team that knows MySQL failover, backup and tuning ships a more reliable product than one learning PostgreSQL on the job.
You are extending an existing MySQL system. WordPress, WooCommerce, Magento or an established internal platform. Do not run two engines to satisfy a preference. Same argument we make in React vs Next.js for startup projects: consistency with what exists usually wins.
Your workload is read heavy, simple and very high volume. Millions of simple lookups, few complex joins, no analytics in the same database. MySQL with read replicas has been exceptional at this for two decades.
You want PlanetScale's branching workflow specifically. Schema branching with deploy requests genuinely reduces migration risk. Neon offers a comparable PostgreSQL model, so this is weaker than in 2023, but still legitimate.
Note what is not on that list: raw speed, cost, and "PostgreSQL is too complex".
What we default to and why
We default to PostgreSQL 17 or 18 on Neon or Supabase, with Drizzle or Prisma, PgBouncer in transaction mode and Row Level Security for tenant isolation. That is the stack behind most of the SaaS products in our portfolio of shipped work.
The reasoning is optionality, not benchmarks. Over a two year SaaS lifespan we can predict roughly 60% of requirements. The other 40% arrives as customer requests: search, location, AI features, reporting, audit trails. PostgreSQL absorbs almost all of those without new infrastructure. MySQL absorbs about half. Every avoided datastore saves roughly two weeks of integration, 70 to 150 USD per month, and one permanent source of incidents.
The counterweight: PostgreSQL demands more operational discipline. Vacuum and bloat awareness, lock timeouts, pooling from day one, statistics maintenance. None of it is hard. All of it must be deliberate. We encode these as project template defaults so nobody has to remember them, the same principle behind our AI powered development workflow: make the correct thing the path of least resistance.
The database is rarely what kills a SaaS. Unindexed queries, missing lock timeouts and undisciplined migrations do, and both engines punish those equally. Pick one, apply the discipline, spend the rest of your energy on the product. For how the whole stack fits together, see choosing an MVP tech stack and our approach to full-stack product development.
Frequently Asked Questions
Is PostgreSQL faster than MySQL? Not in general. For simple lookups and read heavy workloads they sit within a few percent of each other. PostgreSQL wins on complex joins, aggregations and parallel queries; MySQL wins on very high volume simple reads. Query design and indexing matter far more than engine choice.
Can I start with MySQL and move to PostgreSQL later? Yes, but budget three to six weeks for a mid sized product at 10M rows, or roughly 25,000 to 55,000 USD. Moving data is straightforward; rewriting engine specific queries, ORM configuration and test suites consumes the time. Decide properly now rather than assume a cheap switch later.
Does PostgreSQL still have connection limit problems? Yes, and you should plan for it. It uses a process per connection at 5 to 10MB each, so managed instances typically cap around 100 to 500. Use PgBouncer or your provider's pooler in transaction mode and size Node.js pools under 60% of the limit.
Do I need a separate vector database for AI features? Almost certainly not below 50M vectors. pgvector with an HNSW index returns top 10 neighbours over 1M embeddings in roughly 5 to 20ms, fast enough for search, recommendations and retrieval augmented generation. A dedicated vector database early adds cost and a sync problem you do not need.
Is PostgreSQL full text search good enough to replace Elasticsearch? For most SaaS products, yes, up to roughly 5M documents. tsvector with a GIN index plus pg_trgm handles typical product search well. You need Elasticsearch for advanced relevance tuning, faceted navigation at scale, or tens of millions of documents.
Which is better for multi-tenant SaaS? PostgreSQL, mainly because Row Level Security enforces isolation in the database rather than relying on developers remembering a WHERE clause. MySQL has no equivalent. Start with a shared schema and a tenant_id column on either engine, and move to database per tenant only when a contract demands it.
How much does a production database cost per month for a new SaaS? Roughly 19 to 60 USD on Neon, Supabase or Render for a small production instance, rising to 150 to 300 USD with meaningful traffic and a standby. RDS and Aurora start around 70 to 180 USD and roughly double for multi AZ. Engine choice does not change these numbers.
What breaks most often in production, PostgreSQL or MySQL? Neither engine is the usual culprit. The common causes are missing indexes, connection pool exhaustion, and migrations acquiring locks behind long running queries. Set lock_timeout and statement_timeout on every migration, and review EXPLAIN ANALYZE for any query touching over 100k rows.
Should I use an ORM or write raw SQL? Use an ORM or query builder for the 90% of queries that are straightforward, and drop to raw SQL for the 10% that are performance critical. Drizzle and Prisma both make this easy on Node.js. What matters is being able to read the generated SQL and its plan when something is slow.
Is MySQL better for serverless deployments? It handles high connection churn slightly better because it uses threads rather than processes. That advantage mostly disappears with a PostgreSQL pooler in transaction mode or an HTTP driver, both of which Neon and Supabase provide. Serverless alone is not a strong enough reason to choose MySQL.
How do I run schema migrations safely on a live SaaS? Use expand and contract: add the column, backfill in batches, dual write, switch reads, drop the old column in a later release. On PostgreSQL always use CREATE INDEX CONCURRENTLY and set a lock_timeout of a few seconds. On MySQL use gh-ost or pt-online-schema-change for large rebuilds.
When should I add read replicas? When read queries consistently push primary CPU above 70%, or when analytical queries affect transactional latency. Below roughly 5,000 concurrent users this is usually premature; better indexing and caching solve the same problem with less overhead.
Does the database choice affect frontend performance? Indirectly and significantly. Slow queries surface as slow API responses, which show up as slow pages no matter how well the frontend is built. Our guides on React frontend architecture and why React apps feel slow cover the other half.
Related Reading
- How to choose between MongoDB and PostgreSQL for your SaaS
- Node.js backend architecture for a scalable SaaS
- Why your REST API in Node falls over under load
- REST vs GraphQL: how to choose for your next web app
- Is AI generated code production ready?
- Backend development services
- API development services
- Node.js development services