Scaling Your Database: Why Startups Should Go Postgres First
Your app is getting slower. An engineer suggests moving to a different database: MongoDB, DynamoDB, something distributed. It sounds like a big scaling decision, and it comes with a big price tag in time and risk.
My opinion, after a decade of building backends: for most startups it is the wrong move, and it is almost always too early. A well-run Postgres database carries far more load than most startups ever send it. The problem is usually how it is being used, not what it is.
This post gives you the order of moves, the signals that tell you where you are, and the narrow cases where a second database really is justified.
Why "Postgres first"
Three reasons, in plain terms.
It is boring, and boring is good. Postgres is mature, widely understood, easy to hire for, and offered as a managed service by every major cloud. When something breaks at 2 a.m., there is an answer online.
It does many jobs. Transactions, relational data, JSON documents, full-text search, queues, even vector search through extensions. I have written about using Postgres for search before you reach for a separate search engine. Every extra database you run is another thing to back up, secure, monitor and learn.
Switching is expensive and rarely fixes the cause. If the slowness comes from a missing index or a chatty application, a new database inherits the same problem, plus a migration project.
The order of moves
Work down this list. Do not skip to step 6 because it sounds impressive.
Step 1: Indexes and query fixes
Typical symptoms: specific pages or reports are slow, and the database CPU is high while traffic is flat.
What it is: a database index is like the index in a book. Without one, the database reads every page to find what you asked for. Query fixes mean rewriting the few queries that do the most damage, for example one that fetches thousands of rows when the screen shows ten.
Cost: hours to days. Risk: low. This step solves a surprisingly large share of "we need to scale" complaints.
Step 2: Connection pooling
Typical symptoms: errors about "too many connections", slowdowns when traffic spikes or when you add more app servers, even though the database itself is not busy.
What it is: each connection to the database uses memory. A pooler sits in front and lets many app requests share a small number of connections.
Cost: days. Risk: low to medium (some features behave differently through a pooler).
Step 3: Cache hot reads
Typical symptoms: the same data is requested thousands of times (product pages, settings, dashboards) and it rarely changes.
What it is: keep a copy of popular answers in fast memory for a short while. Your app asks the cache first and the database only when needed.
Cost: days to weeks. Risk: medium. You now have to decide how stale an answer is allowed to be. If five-minute-old data is fine for a screen, cache it. If it is a balance or a stock count, be careful. See the scaling roadmap for where this fits.
Step 4: Read replicas
Typical symptoms: the database is busy mostly with reads (reporting, analytics, search pages), and writes are comparatively few.
What it is: a live copy of your database that serves read-only traffic, so heavy reports do not slow down signups and checkouts.
Cost: a day or two of setup plus roughly the price of a second database. Risk: medium. Replicas lag slightly behind the main database, so a user can save something and not see it for a moment. Your app needs to handle that.
Step 5: Partitioning (and archiving)
Typical symptoms: a few tables have grown to hundreds of millions of rows, and maintenance, backups or queries on them are slow even with good indexes.
What it is: splitting one huge table into smaller pieces, usually by date, so queries touch only the recent piece and old data can be archived or dropped cheaply.
Cost: weeks. Risk: medium to high, because it changes how the table is built. Worth it only when the table size is the real issue.
Step 6: A different datastore, for a specific job
Only after steps 1 to 5. And only for a job you can name. More on this below.
For your CTO or engineers
- Step 1: enable
pg_stat_statements, sort by total time, runEXPLAIN (ANALYZE, BUFFERS)on the top ten. Look for sequential scans on large tables, N+1 queries from the ORM, missing composite indexes, unused indexes that slow writes (pg_stat_user_indexes), and bloat from long-running transactions. - Step 2: PgBouncer in transaction mode or a managed proxy such as RDS Proxy. Watch for session-level features (prepared statements, advisory locks,
SET) that break in transaction pooling. Size the pool to a small multiple of CPU cores, not to the number of app threads. - Step 3: cache-aside with explicit TTLs; invalidate on write for anything user-visible. Protect against stampedes on popular keys.
- Step 4: route by intent, not by query. Read-your-writes paths (right after a mutation) go to the primary. Monitor replica lag and fail reads over to the primary when lag exceeds your threshold.
- Step 5: declarative range partitioning by time; make sure the partition key appears in primary and unique keys, and automate creation and detachment of partitions.
- Before any of this, also check vertical scaling: a bigger instance is a legitimate, cheap, reversible step while you do the real work.
Signals: which step are you on?
| What you see | Likely step |
|---|---|
| A few specific screens or reports are slow | 1. Indexes and queries |
| "Too many connections" errors; spikes when servers scale out | 2. Connection pooling |
| Same data fetched repeatedly, rarely changes | 3. Caching |
| Reports slow down customer-facing actions | 4. Read replica |
| Giant tables; maintenance and old-data queries crawl | 5. Partitioning |
| A job Postgres is bad at, proven by measurement | 6. Second datastore |
The rule: measure before you move. If you cannot point to the query, the table or the graph that justifies a step, you are guessing.
When is a second database justified?
Not "when we get big". When you can complete this sentence: "Postgres cannot do X at the volume we need, and we measured it." Real examples:
- Very high-volume event or time-series data (millions of events per hour) where append-only, columnar or time-series stores are cheaper and faster.
- Heavy search with relevance tuning, once Postgres full-text search stops being good enough.
- A cache or short-lived data like sessions or rate-limit counters.
- Analytics that should live in a warehouse, away from your production database.
- Truly global, multi-region writes with strict latency requirements. This is rare for startups.
Notice that in each case the new datastore is added next to Postgres for one job. Postgres stays the system of record. That is very different from "replace the database".
Example (illustrative)
A 20-person B2B SaaS startup with an engineer proposing a move to a NoSQL database because "the dashboard takes 12 seconds". Hypothetical numbers, but a common story.
Investigation takes two days. The dashboard runs one query that scans a 40-million-row events table with no index on the customer column. Adding a composite index drops it from 12 seconds to under 1. A week later, "too many connections" errors appear at peak, and a connection pooler fixes them. The monthly report that slowed the app each morning moves to a read replica. Total: roughly two engineer-weeks and one extra database instance.
The migration they nearly started would have taken several months, and the dashboard would still have needed an index.
Six months later the events table passes 400 million rows and storage cost is climbing. They partition it by month and archive anything older than a year. Still Postgres. If they later need real-time product analytics across billions of events, that is the moment for a warehouse beside Postgres, not instead of it.
When NOT to do this
Postgres first is a default, not a religion. Think twice if:
- Your data is genuinely not relational and never will be, for example a pure key-value workload at enormous scale from day one. Rare.
- You already run a different database well and your team knows it deeply. Do not migrate away just to follow advice.
- A regulatory or contractual requirement dictates specific infrastructure.
- Your database is already the managed, multi-region type your product needs, and the numbers support it.
- You are about to hit a hard limit this quarter that steps 1 to 5 cannot address. Then plan the second datastore properly, but still do the cheap steps first so you migrate a healthy system.
Common mistakes
- Skipping to the exciting step. Teams jump to sharding or a new database without ever reading a query plan.
- Adding indexes everywhere. Each one slows writes and uses storage. Add the ones the slow queries need.
- Caching without a freshness rule. Stale data in a billing screen becomes a support problem.
- Using a read replica for everything. Reads right after writes will show old data.
- Splitting into microservices to fix a database problem. It usually spreads the problem across more places. See the distributed monolith trap.
- Not measuring. Without baselines you cannot tell if a step worked.
- Treating the fix as one-off. Queries get slower again as data grows. Watch the top queries monthly.
Checklist
- Top 10 slowest queries by total time identified
- Missing indexes added, unused ones reviewed
- Connection limit headroom checked; pooler in place if needed
- Cacheable read paths listed, with an agreed staleness limit
- Reporting traffic separated from customer traffic (replica) if reads dominate
- Largest tables reviewed for partitioning or archiving
- Any proposal for a new datastore written as "X, at volume Y, measured"
- Monthly review of database metrics (CPU, connections, slow queries, storage growth)
Not sure which step you are on?
This is the core of my Backend Scaling work. I look at your slow queries, connection patterns and growth, and tell you which of these steps you actually need, and which you can safely skip. If you are staring at a database decision, book a free 30-minute technical review via /contact. For the wider picture, see the Scaling & Reliability guide, and the backend scaling checklist for a broader audit list.
FAQ
How big can a single Postgres database get before it is a problem?
There is no single number, because it depends on the workload, the hardware and how well queries are written. Many startups run comfortably on one primary with a replica for years. Measure query time, connection usage and storage growth rather than watching a user count.
Should we use MongoDB or DynamoDB because we expect to scale?
Only if you can name a specific access pattern that suits them and Postgres does not. Expecting growth alone is not a reason. Postgres handles JSON documents too, so you can keep flexibility without giving up transactions.
Is sharding ever the right answer for a startup?
Rarely, and late. It adds major complexity to every query and every migration. Exhaust indexes, pooling, caching, replicas and partitioning first, and consider moving data that does not need to be in the main database before you split the main database.
What does this cost compared to a migration?
The first three steps are mostly engineering time measured in days. A replica roughly doubles that database's bill. A full migration to a new datastore is usually months of work plus running both systems in parallel, so the comparison is rarely close.
Next step
- More on this topic: Scaling & Reliability guide
- Working on this now: Backend System Scaling
- See it in practice: Global Fintech Infrastructure