The honest answer is Postgres. It is Postgres for the overwhelming majority of products, at the overwhelming majority of stages, and it stays Postgres far longer than the people writing about databases would like you to believe.
That is not a satisfying article, so most people writing this one skip past it and present eight options as if they were peers. They are not peers. One of them is a general-purpose default that will carry your product to a scale most startups never reach, and the others are specialized tools that solve a specific problem in exchange for a second system to operate.
The interesting question is therefore not which database. It is when the exception is real, and what the exception costs. Because the cost is never the thing people compare. They compare query latency and feature lists. What they actually pay is a second backup strategy, a second restore drill nobody runs, a second set of access controls, a second failure mode at 3am, a consistency boundary between two stores that your application code now has to reason about, and a hiring constraint you discover when the one engineer who understood it leaves.
Every exception below is sometimes correct. None of them is correct because the benchmark looked good.
Key takeaways
- A second database costs a second backup, a second restore drill, a second on-call surface, and a dual-write consistency problem your application now owns. Price that before you compare features.
- Postgres stops being the right answer at specific, nameable boundaries: single-primary write throughput, columnar scan workloads, cache-shaped access patterns, and per-user local data. Outside those, adding a system is a choice you are making for reasons other than capability.
- The correct time to add the exception is when you can name the query or workload Postgres could not serve and show the plan or metric that proves it. Not before.
- Choosing something other than Postgres at the start is usually a hiring and ecosystem decision disguised as a technical one, and it is the decision that is hardest to reverse.
What a second database actually costs
Write this list down before you evaluate anything, because it is the column missing from every comparison table.
A second durability story. Backups that run, restores that have been tested, and a documented recovery point and recovery time. Most teams that add a second store never run a restore drill against it. The second store is also, reliably, the one holding data that is hard to reconstruct.
A second consistency boundary. The moment two systems hold related state, you own the problem of keeping them agreed. Dual writes fail halfway. A write to Postgres that succeeds and a write to the search index that does not leaves you serving stale results with no error anywhere. The correct fix is change data capture or an outbox table, and both of those are infrastructure you now run. Change data capture tools covers that machinery.
A second operational vocabulary. Different failure modes, different metrics, different tuning parameters, different upgrade semantics. Your on-call engineer has to learn all of it, and will learn it during the incident.
A second security surface. Network policy, credentials, rotation, audit logging, and whatever your compliance obligations require, duplicated.
A hiring and onboarding tax. Every new engineer learns two data models instead of one. Every architectural discussion has an extra axis.
Loss of joins. This is the one that bites quietly. Data split across two systems cannot be joined by the database, so it gets joined in application code: fetch from A, loop, fetch from B. That loop is an N plus one query pattern with a network hop, and it appears in your latency profile months later as an unexplained tail.
None of this argues you never add a second store. It argues that the threshold should be a named, demonstrated limit rather than an anticipated one.
Where Postgres actually stops
Four boundaries, each with a mechanism. If your situation is not one of these, the exception is probably not real yet.
Single-primary write throughput
Postgres accepts writes on one primary. Read replicas scale reads, not writes. The write path is bounded by WAL flush throughput, by the cost of index maintenance on every insert, and by autovacuum keeping up with the dead tuples updates generate. You can push this a long way with fewer indexes, batched inserts, partitioning, and moving high-churn counters out of hot rows. When you cannot push further, the answer is sharding, and sharding is a rewrite of every query that crosses a shard boundary.
The honest observation is that very few startups reach this. Most teams that believe they have are actually hitting lock contention on a small number of hot rows, or an unbatched write pattern, and both of those are fixable without leaving.
Needs first-hand data: Before concluding you have hit a write ceiling, sample
pg_stat_activityevery second for an hour during peak and count rows wherewait_event_typeisLock, grouped bywait_event. Separately, record WAL bytes generated per minute frompg_stat_wal. Lock waits dominating means contention you can fix in the schema; WAL throughput flat at a ceiling while latency climbs means a genuine hardware or single-primary limit.
Columnar scan workloads
Postgres stores rows together on 8KB pages. Summing one column across ten million rows reads every page holding those rows, including all the other columns. Analytics queries pay I/O proportional to table width rather than query width, and no index changes that fact because the heap fetch still reads whole pages.
You can stretch it with partitioning, BRIN indexes on append-only time-ordered data, and rollup tables. Rollup tables work, and what you have built when you finish is a narrow sorted pre-aggregated store, which is a column store implemented in a row store with none of the compression. The failure that eventually forces the move is not slow dashboards; it is a long analytics query holding a snapshot that prevents autovacuum from reclaiming dead tuples on your transactional tables, so a reporting query degrades checkout. Postgres vs ClickHouse vs Tinybird works through exactly where that line sits.
Cache-shaped access patterns
Some access patterns are not database queries. A rate limiter counter, a session token lookup, a leaderboard, a work queue: these are high-frequency, low-value, expiring reads and writes. Putting them in Postgres means every one generates WAL, generates dead tuples on update, and consumes a connection from a pool sized for transactional work. The mismatch is not speed, it is that durable transactional storage is the wrong tool for data you would not miss.
Per-user or per-device local data
If the data belongs to one user and the read latency is dominated by the network round trip to a central primary, the fix is not a faster database, it is a closer one. That is a different architecture rather than a different product, and it is covered in edge databases.
Needs first-hand data: Before you add anything, run
pg_stat_statementsfor a full week on production and pull the top twenty statements by total execution time, then by call count. RunEXPLAIN (ANALYZE, BUFFERS)on the top three. Record the shared block hits versus reads. If the expensive queries are cache-shaped or columnar, you have found a real boundary; if they are missing an index or doing an N plus one, you have found a bug, and adding a second database will not fix it.
Postgres

The general-purpose relational database with the widest capability surface of anything in this list. Transactions, constraints, joins, JSONB for the parts of your schema that are genuinely unstructured, full-text search, and an extension mechanism that lets it absorb workloads other databases exist to serve: vectors through pgvector, geospatial through PostGIS, time-series through TimescaleDB. Choosing it is choosing to defer the second-database decision until you can justify it.
Pros
- One system covering relational, document, full-text, geospatial, vector and time-series workloads through extensions, so most “we need a specialized store” moments resolve into
CREATE EXTENSION - Constraints, foreign keys and transactions mean the database enforces invariants your application code would otherwise enforce inconsistently
- The deepest hiring pool, the best tooling, and every ORM, migration tool and managed provider targets it first
- JSONB gives you document-style flexibility inside a relational database, with indexes, which removes the most common reason teams reach for MongoDB
Cons
- One primary accepts writes, so scaling writes beyond a single machine means sharding and a rewrite of cross-shard queries
- MVCC means updates create dead tuples, and autovacuum falling behind on a high-churn table is the most common self-inflicted Postgres incident
- Row storage makes analytical scans structurally expensive, and no amount of indexing changes that
Best for: Essentially every startup at essentially every stage, until you can name the specific query or throughput limit it failed to meet.
Pricing: No licence cost. You pay for whatever managed provider or hardware runs it, which is covered in managed Postgres providers.
MySQL
The other mature open-source relational database, with a large operational ecosystem, excellent replication tooling, and the horizontal sharding path that Vitess provides. Choosing it over Postgres in 2026 is usually a team decision rather than a capability one, and that is a legitimate reason. It becomes a capability decision when you need Vitess-style sharding or your hosting platform makes it the path of least resistance.
Pros
- Mature replication and a well-trodden operational path for large deployments
- Vitess gives an actual horizontal sharding story that Postgres does not have an equivalent for in the open-source ecosystem
- Some managed platforms build sophisticated schema migration workflows on top of it, which is a real operational advantage on large tables
Cons
- A much thinner extension ecosystem, so the workloads Postgres absorbs through extensions become separate systems here
- Historically looser behaviour around data types, defaults and constraint enforcement means more invariants live in application code
Best for: Teams whose existing expertise is MySQL, or whose platform choice brings Vitess-style sharding and migration tooling with it.
Pricing: No licence cost for the community edition. You pay for the managed service or the hardware.
SQLite
The database that runs inside your process with no server, no connection and no network hop. For a long time this was a local-development and mobile story; replication layers such as LiteFS changed that by shipping the write-ahead log to read replicas, so a single-writer SQLite database can serve reads from many application instances. The result is a genuinely different latency profile, because a read is a function call rather than a round trip.
Pros
- Reads are in-process with no network, which removes an entire tier of latency and an entire class of connection problems
- Creating a database is creating a file, which makes database-per-tenant realistic at counts that would be absurd for Postgres
- Replication layers like LiteFS let read replicas follow a single primary, so horizontal read scaling does not require a server database
Cons
- One writer at a time per database, and no configuration changes that. A write-heavy workload on a single logical database hits a ceiling you cannot tune past
- Write forwarding to the primary means writes still pay the network, so the latency win applies to reads only
- The server-side ecosystem is thin: weaker analytical SQL, fewer migration and monitoring tools, and operational patterns you will be inventing
Best for: Read-heavy applications, especially per-tenant or per-user databases, where reads dominate and write concurrency per database is naturally low.
Pricing: No licence cost. Hosted offerings meter on rows read and written plus storage.
MongoDB
A document database where the unit of storage is a BSON document and the schema is enforced by your application unless you opt into validation. Its real advantage is not flexibility, which JSONB also provides, but that documents co-locate related data so a read that would be a join in a relational model is a single document fetch. That advantage is real when your access pattern genuinely matches a document boundary and evaporates when it does not.
Pros
- Co-locating related data in one document turns a multi-table join into a single read for access patterns shaped that way
- Horizontal sharding is built into the product rather than being an add-on architecture
- Aggregation pipelines are expressive for the shapes they cover, and change streams give a clean event feed
Cons
- Schema enforcement is optional, which means in practice a collection accumulates several document shapes over years and your application handles all of them forever
- Data that does not fit inside one document boundary requires application-side joins, and choosing the wrong boundary early is very expensive to change later
- Postgres JSONB covers the majority of the flexibility argument without giving up constraints and joins, which removes the most common reason to pick this
Best for: Products whose data genuinely is document-shaped with stable aggregate boundaries, and teams with existing operational depth in it.
Pricing: No licence cost for the community edition; managed offerings meter on cluster size or on serverless read and write units plus storage.
DynamoDB
A managed key-value and document store with predictable access-by-key performance and capacity that scales without you resizing anything. It is the clearest example in this list of a database that trades generality for operational silence. You design access patterns first and the data model second, which is a discipline, not a limitation, provided your access patterns are actually stable.
Pros
- Genuinely elastic with no instance to size and no connection pool, which removes several whole categories of incident
- Performance by key is predictable in a way that a database with a query planner is not, because there is no plan to flip
- Operationally quiet: no vacuum, no failover semantics to learn, no version upgrades to schedule
Cons
- A new access pattern means a new index or a table redesign, and ad hoc questions are effectively unavailable, so analytics has to be exported elsewhere
- No joins and limited transaction scope, so relational integrity lives in application code where it will eventually be violated
- Deeply tied to one cloud, and the data model does not translate cleanly to anything else, which makes it the hardest exit in this list
Best for: High-volume operational workloads with stable, well-understood key-based access patterns inside AWS.
Pricing: Metered per read and write request unit plus stored data, with a provisioned capacity mode as an alternative meter.
Redis

An in-memory data structure server. Strings, hashes, sorted sets, streams, with atomic operations and optional expiry, at latencies a disk-backed database cannot approach because it does not touch disk on the read path. It is the right answer for cache-shaped workloads and the wrong answer as a primary store, and the second half of that sentence is worth being blunt about.
As a primary store the problem is durability semantics. Point-in-time snapshots lose everything since the last snapshot. Append-only file persistence with per-second flushing loses up to a second, and per-write flushing costs you the performance you bought it for. Replication is asynchronous, so a failover can lose acknowledged writes. None of that matters for a cache, where the recovery procedure is to repopulate from the source of truth. All of it matters for data that has no source of truth behind it.
Use it for sessions, rate limiting, leaderboards, work queues and cache. Put the thing you cannot lose in Postgres. The one honest exception is data that is genuinely ephemeral by nature, where losing a second of it during a failover is a shrug rather than an incident.
Pros
- Data structures with atomic operations solve rate limiting, leaderboards and queues in a few commands rather than a table and a lock
- In-memory reads remove the storage layer entirely from the latency budget for cache-shaped access
- Taking session, counter and queue traffic out of Postgres protects your connection pool and your vacuum workload
Cons
- Durability is configurable and every configuration trades data loss against throughput, so there is no setting that makes it a safe system of record
- Asynchronous replication means a failover can lose acknowledged writes, which is acceptable for a cache and not for anything else
- Your working set must fit in memory, so cost scales with data volume in a way disk-backed storage does not
Best for: Caching, sessions, rate limiting, queues and leaderboards alongside a durable primary database, not instead of one.
Pricing: Metered on provisioned memory and throughput, or per-request on serverless offerings. See managed Redis and caching providers.
ClickHouse

A column-oriented database built for analytical scans. Data lands in sorted, compressed parts with each column in its own file, so a query reads only the columns it names, and materialized views pre-aggregate rows as they arrive rather than at read time. For a startup this is the exception with the clearest trigger: you have an in-product analytics surface or an event volume that makes Postgres aggregation untenable.
Pros
- Columnar storage with high compression means analytical scans read a small fraction of the bytes a row store would
- Materialized views are incremental on insert, so rollups are maintained as data arrives instead of recomputed on a schedule
- Retaining a long history is economically viable in a way it is not on row storage
Cons
- It is not a transactional database. Updates and deletes are asynchronous mutations rather than transactions, so it cannot be your system of record
- It is built for a small number of large queries, so thousands of tenants refreshing a dashboard at once is a concurrency problem, not a scan-speed problem
- The sort key is the design rather than a tuning knob, and a wrong choice means rewriting the table
Best for: Event and analytics workloads alongside Postgres, once aggregation queries are measurably damaging your transactional database.
Pricing: No licence cost for the open-source version; managed offerings meter on compute and compressed storage.
How to choose
1. Start with Postgres. Do not treat this as a placeholder decision. It is the decision. Build the product on it and revisit only when something specific fails.
2. When you feel the pull toward a second store, name the query. Write down the exact statement or workload, its plan, and the metric that makes it unacceptable. If you cannot produce all three, the exception is anticipated rather than real.
3. Check whether an extension closes the gap. Vectors, geospatial, time-series and full-text all have Postgres extensions that remove the need for a second system entirely. Postgres extensions for production workloads covers which ones your provider will actually let you install.
4. If the gap is real, add the smallest thing that closes it. A cache for cache-shaped access. A column store for columnar scans. Not a platform, not a replacement.
5. Decide the synchronization mechanism before you provision anything. Change data capture from Postgres, or an outbox table, or an explicit batch job. Dual writes from application code are the option that looks simplest and fails silently.
6. Write the exit path down. For every store you add, one paragraph on how the data gets out if the vendor, the pricing or the fit changes. The stores with no good answer are the ones to be most careful about adding.
Needs first-hand data: Before adding a cache, quantify what it would remove. Take a week of
pg_stat_statementsoutput and tag each statement as cache-shaped (session lookup, counter, rate limit, feed fetch by key) or not, then sumtotal_exec_timeandcallsfor the cache-shaped set as a share of the whole. If that share is small, a cache buys you little and costs you a second system.
| Option | What it is | Add it when |
|---|---|---|
| Postgres | General-purpose relational | Always, first, by default |
| MySQL | General-purpose relational | Your team or platform already lives there |
| SQLite | Embedded, in-process | Reads dominate and per-tenant databases fit your model |
| MongoDB | Document store | Your aggregates genuinely fit one document boundary |
| DynamoDB | Managed key-value | Stable key access patterns at volume, inside AWS |
| Redis | In-memory structures | Cache, sessions, queues, counters, never the source of truth |
| ClickHouse | Column store | Analytical scans are measurably damaging your primary |
Frequently asked questions
Is Postgres really enough at scale?
For longer than almost anyone needs. The limits are specific: single-primary write throughput, row-storage cost on analytical scans, and vacuum behaviour on high-churn tables. Teams usually hit a missing index, a hot row, or an N plus one before they hit any of those. Diagnose which one you have before concluding the database is the problem.
Should I use MongoDB for a schema that changes a lot?
Probably not, because Postgres JSONB gives you the same flexibility with indexes, constraints on the structured parts, and joins. Schema change speed is a migration tooling problem more than a database problem. Pick MongoDB when your reads genuinely fetch one whole aggregate, not when your schema is undecided.
Can I use Redis as my main database?
You can, and the durability semantics mean you should not. Snapshots lose everything since the last one, append-only persistence trades loss window against throughput, and asynchronous replication can drop acknowledged writes on failover. That is a fine bargain for a cache with a source of truth behind it and a bad one for the source of truth.
When do I add a data warehouse?
When your analytical questions come from humans rather than from your product, and they are ad hoc rather than predefined. In-product dashboards and internal analysis are different workloads with different right answers. Data warehouses for startups covers the second one.
How much does it matter which ORM I pick?
Less than the database choice and more than people expect, because the ORM determines how visible your queries are. An ORM that hides query generation makes the N plus one problem invisible until it is in production. TypeScript ORMs covers that tradeoff.
Related reading
- Best managed Postgres providers — the hub, and where to actually run the default answer.
- Postgres vs ClickHouse vs Tinybird — the analytics boundary in detail, with the failure modes on each side.
- Best Postgres extensions for production workloads — the extensions that remove the need for a second database.
- Best serverless databases — cold starts, scale-to-zero economics and the connection model.
- Best managed Redis and caching providers — where the cache-shaped workload should actually live.
- Best change data capture tools — how to keep two stores agreed without dual writes.