Scaling Supabase for Startups: Pooling, Indexes and Costs
On this page
Scaling Supabase without surprises: how connection pooling, indexes and compute sizes behave as traffic grows, and what each step costs.
Supabase handles real production traffic, and the parts that break first are rarely Postgres itself. Scaling Supabase comes down to three things: how your application opens connections, whether your busiest queries have indexes, and which compute size you are paying for. Each one is cheap to fix early and expensive to discover during a traffic spike.
This guide is for founders and small teams running a live product on Supabase, including apps that started in Lovable, Bolt or Cursor. It covers the connection limits at each compute size, which pooler mode to use where, the indexes that matter as tables grow, what the bill looks like as usage climbs, and the point at which moving off is worth considering.
Key takeaways
- Connection limits come from your compute size, not your plan: Micro allows 60 direct connections and 200 pooler clients, while 2XL allows 380 and 1,500.
- Serverless code should connect through the pooler in transaction mode with an application-side pool of one, because that mode drops prepared statements and session state.
- Most reports that "Supabase is slow" turn out to be a missing index or a policy re-evaluated once per row, not a hardware problem.
- The Pro plan starts at $25 a month and includes $10 of compute credits, enough for one Micro instance. Everything past the included quotas is billed by usage.
- Raise compute last. A bigger instance hides a bad query instead of fixing it, and you pay for it every hour.
Scaling Supabase: what breaks first
The order is almost always connections, then queries, then compute. Projects fall over because a serverless function opened four hundred connections, or because one table scan that used to take 40 milliseconds now takes nine seconds, long before 1 GB of RAM is the real constraint.
That order matters because the fixes get progressively more expensive. Changing a connection string is free. Adding an index takes an afternoon. Moving up two compute sizes costs money every hour, forever, and needs a short window of downtime to apply.
The sequence is the same one in our playbook for taking an MVP to production without a rewrite: find the actual bottleneck, fix the cheap thing first, and buy hardware only once you know what you are buying it for.
How many connections does a Supabase project get?
Every project runs its own Postgres instance, and the limits are set by compute size rather than by plan. Two numbers matter: direct connections, which is how many sessions Postgres itself will accept, and pooler clients, which is how many application connections the shared pooler will hold in front of it.
| Compute size | Monthly price | Memory | Direct connections | Pooler clients |
|---|---|---|---|---|
| Micro | $10 | 1 GB | 60 | 200 |
| Small | $15 | 2 GB | 90 | 400 |
| Medium | $60 | 4 GB | 120 | 600 |
| Large | $110 | 8 GB | 160 | 800 |
| 2XL | $410 | 32 GB | 380 | 1,500 |
These figures come from Supabase's published pricing. The ladder continues to 16XL at 500 direct connections and 12,000 pooler clients, and the compute and disk documentation adds the disk throughput and IOPS that come with each size, which is the limit you meet on write-heavy workloads.
Why "too many connections" appears at low traffic
Because connection count tracks the number of running application instances, not the number of users. A serverless host keeping 40 warm instances alive, each holding a driver pool of 10, is asking a database configured for 60 connections to accept 400.
Three details cause most of it:
- Driver defaults. Postgres.js defaults to a pool of 10 per client, and how many warm instances your host keeps is not something you control.
- The Data API competes for the same budget. PostgREST, Auth, Storage and Realtime all hold Postgres connections of their own. Supabase suggests keeping the pooler's share below roughly 40% of your maximum connections if your app leans on the Data API, and up to 80% if it does not.
- Sessions stuck
idle in transaction. A transaction nobody committed holds its locks and its connection slot indefinitely. Cancelling the query does nothing, because there is no query running; the session has to be terminated.
Transaction mode, session mode or a direct connection?
Match the mode to where your code runs. Serverless and edge functions belong on the shared pooler in transaction mode, persistent backends on a direct connection, and anything that needs session state or prepared statements on session mode or a direct connection.
| Connection method | Port | Use it for | Main limitation |
|---|---|---|---|
| Direct connection | 5432 | Persistent backends, migrations, pg_dump |
IPv6 only unless you add the IPv4 add-on |
| Shared pooler, session mode | 5432 | IPv4-only backends, BI tools, database GUIs | Holds one server connection for the whole session |
| Shared pooler, transaction mode | 6543 | Serverless and edge functions | No prepared statements, cursors or session state |
| Dedicated pooler | 6543 | Latency-sensitive apps on paid plans | Transaction mode only |
The trap is transaction mode combined with a driver that assumes a persistent session. Supabase's guide to connecting to your database lists what stops working: prepared statements, cursors that outlive their transaction, listen and notify, temporary tables, session-level advisory locks and query pipelining.
In a serverless function, create the client once at module scope and keep it small:
import postgres from 'postgres'
// One client per warm instance, created once, not per request.
export const sql = postgres(process.env.DATABASE_URL, {
max: 1, // let the pooler do the pooling
prepare: false, // transaction mode has no prepared statements
ssl: 'require', // refuse to fall back to plaintext
})
Prisma expresses the same thing as pgbouncer=true on the connection string, asyncpg as statement_cache_size=0 and JDBC as prepareThreshold=0. Raise max above 1 only when you have evidence that concurrent invocations on one instance are queueing for the connection.
Which indexes matter as you grow
The ones behind your most frequent filters, joins and sorts. A table that felt instant at 5,000 rows and crawls at 500,000 is usually missing a single B-tree index, and the fix is one statement.
Start with unindexed foreign keys
Postgres indexes primary keys automatically but not the columns that point at them, so a generated schema often has organization_id or user_id with no index at all. Every tenant-scoped query then scans the whole table. Supabase's Performance Advisor flags exactly this, along with duplicate indexes, unused indexes and tables with no primary key, so check it before you start guessing.
Add an index without locking the table
Use create index concurrently. A plain create index takes a lock that blocks writes for as long as the build runs, which on a large production table is an outage.
-- Blocks writes until it finishes
create index idx_invoices_org_id on invoices (organization_id);
-- Builds alongside live traffic: slower, but safe
create index concurrently idx_invoices_org_id on invoices (organization_id);
Two caveats from the PostgreSQL CREATE INDEX documentation: a concurrent build cannot run inside a transaction block, and if it fails it leaves an invalid index behind that you have to drop by hand. Migration tools wrap statements in a transaction by default, so this needs handling deliberately, which our notes on keeping Supabase schema changes in versioned SQL cover.
Partial and composite indexes
When a query always filters on the same subset of rows, index only that subset. A partial index limited to where status = 'active' is smaller, faster to scan and cheaper to maintain than one covering every row in the table. For queries that filter on two columns together, one composite index on both beats two separate single-column indexes, and the column order should follow the where clause.
Indexes are not free. Each one adds write cost and storage, and the planner will ignore an index it judges unhelpful, so add them against queries you have measured rather than on principle.
Row Level Security is part of the query plan
A policy is a where clause appended to every query against that table, so a slow policy is a slow query. The usual mistake is calling auth.uid() directly inside a policy, which makes Postgres evaluate it once per row; writing it as (select auth.uid()) lets the planner evaluate it once for the whole statement. Any column a policy filters on needs an index as well. Our walkthrough of how to scope rows to the right user covers how to write them; the performance rule is simply that policies are queries.
What does Supabase cost as you grow?
The subscription is the small part. Pro starts at $25 a month and includes $10 of compute credits, enough to cover one Micro instance; past that you pay for compute by the hour and for disk, egress, users and storage by the unit.
The meters that move for a growing SaaS product:
- Compute, billed hourly per instance, from about $10 a month at Micro to $410 at 2XL.
- Disk, 8 GB included per project, then $0.125 per GB.
- Egress, 250 GB included, then $0.09 per GB.
- Monthly active users, 100,000 included, then $0.00325 each.
- File storage, 100 GB included, then $0.0213 per GB.
- Point in Time Recovery, $100 a month per 7 days of retention, on top of the daily backups Pro keeps for 7 days anyway.
A worked example: a product on Pro with one Small instance, 40 GB of disk and 400 GB of egress pays $25 for the plan, $5 for compute ($15 less the $10 credit), $4 for the 32 GB of extra disk and $13.50 for the 150 GB of extra egress. That is roughly $47.50 a month. The same product on a Large instance with Point in Time Recovery turned on lands nearer $250.
Two cost controls are worth knowing. Pro ships with a spend cap enabled, which stops usage beyond the included quotas instead of billing for it, so leaving it on protects the invoice and accepts the outage. Preview branches are billed hourly like any other compute, so they keep charging until somebody deletes them.
Cutting the bill mostly means cutting egress and compute, and caching does both: a response served from cache is a query your database never runs and, at the edge, bytes that never leave your project. Our guide to adding a cache without serving stale data covers where each layer belongs.
When should you move off Supabase?
Later than most founders expect, and rarely because of Postgres. The trigger is usually a workload the platform's shape does not suit rather than a limit you have actually hit.
- Sustained write throughput that needs Postgres tuned by hand, including connection, autovacuum and WAL settings beyond what the platform exposes.
- Compliance requirements beyond the Team plan and the HIPAA add-on, or procurement that insists the database runs in your own cloud account.
- Cost at the top of the ladder. At 16XL you are paying several thousand dollars a month for compute alone, and self-managing Postgres starts to look reasonable if you have someone to run it.
The reassuring part is that Supabase is Postgres. Moving means a dump and restore plus replacing Auth, Storage and the Data API, which is real work, but it is a migration rather than a rewrite. Keep your data access in plain SQL where you reasonably can and that door stays open.
Common mistakes
- Pointing serverless functions at the direct connection string and exhausting the database's connection slots.
- Leaving the driver's default pool size in place, so every warm instance quietly reserves ten connections.
- Upgrading compute to fix a slow query. The query is still slow, and now it costs more per hour.
- Shipping a schema with no index on the tenant or owner column that nearly every query filters by.
- Calling
auth.uid()directly in a policy, so it runs once per row instead of once per statement. - Running
create indexwithoutconcurrentlyon a live table during business hours. - Switching the spend cap off without setting a billing alert, then meeting the invoice at the end of the month.
Frequently asked questions
Is Supabase production ready?
For the great majority of SaaS products, yes. Each project is a dedicated Postgres instance with daily backups retained for 7 days on Pro, and Point in Time Recovery is available as an add-on. The limits you meet first are connection counts and query plans, and both of those are yours to manage rather than platform ceilings.
Does upgrading compute fix a slow query?
Usually not. A sequential scan over a million rows is still a sequential scan on a bigger machine, only faster in proportion to the extra CPU and disk throughput. Check the Performance Advisor and the Query Performance report first, add the index, and upgrade when the profile is clean and CPU is genuinely the constraint.
Do I need the IPv4 add-on?
Only if you need a direct connection or the dedicated pooler from an IPv4-only network. The shared pooler is reachable over IPv4 on every plan, in both session and transaction mode, which covers most cases without paying for anything. The add-on also swaps the project's IPv6 record for an IPv4 one rather than serving both, so enabling it is a change, not an addition.
How do I find out which query is slow?
Start with the Query Performance report in the dashboard, which reads pg_stat_statements and ranks queries by total time. Run explain analyze on the worst offender to see whether it is scanning a table, and check pg_stat_activity for sessions stuck idle in transaction. Fix the slowest query, measure again, repeat.
Can Lovable or Bolt handle this for me?
They write plausible SQL and connection code, and the output is a fair first draft. What they will not tell you is that your connection string is the wrong one for a serverless host, or that a policy filters on an unindexed column. Those are decisions about how the project is deployed, not about the schema.
Where to start
Work through it in cheap-to-expensive order. Check which connection string each part of your app uses and fix the mismatches, run the Performance Advisor and index what it flags, review your policies for per-row function calls, then look at compute. Most projects find the first two steps buy another year of growth.
If you would rather have senior engineers run that review and hand back a project that holds up under load, AlgoSmiths works on fixed-price projects with a working demo every week. Our backend and API engineering work is where this layer gets built and documented.