Supabase itself is usually not the slow part. A status page settles the platform half of the question in seconds; the query half lives in your schema, where the 21 AI-built apps AxonBuild audited kept turning up the same four causes: an unpooled connection, a missing index, an unbounded select, and an N+1 loop.

Is Supabase itself slow, or is it your queries?

Check Supabase’s status page first, and check it literally: does it show a live incident for compute, database, or the region your project runs in. On an ordinary day it’s green, and if it’s green, the platform isn’t your problem; the next twenty minutes belong in your own code, not a support ticket, which is the same split that settles whether Supabase is safe: the infrastructure passes its audits and the policies on your tables are yours. If it’s red, stop reading and wait. No schema fix outruns an actual outage.

What you're seeing
Where the answer actually lives
Every request across the whole project is slow, including ones that touched no data yesterday
Status page first, region-wide incident, before touching a single query
One query, one endpoint, or one page is slow while the rest of the app is fine
Your schema: an unpooled connection, a missing index, or select(*) somewhere on that path
Things were fine, then got slow the week signups or data grew
A wiring gap that only fires past a row-count or concurrency threshold
What you're seeing
Every request across the whole project is slow, including ones that touched no data yesterday
One query, one endpoint, or one page is slow while the rest of the app is fine
Things were fine, then got slow the week signups or data grew
Where the answer actually lives
Every request across the whole project is slow, including ones that touched no data yesterday
Status page first, region-wide incident, before touching a single query
One query, one endpoint, or one page is slow while the rest of the app is fine
Your schema: an unpooled connection, a missing index, or select(*) somewhere on that path
Things were fine, then got slow the week signups or data grew
A wiring gap that only fires past a row-count or concurrency threshold

Supabase slow queries: the four AI-generated causes

The Performance & Scale pillar landed at 53.3 out of 100 across the 21 third-party apps AxonBuild audited in June and July 2026, and every recurring finding behind that number sat in the app’s own schema rather than Supabase’s infrastructure: an unpooled connection opened per request, a foreign key with no index, a query fetching every column with no limit, and a loop firing one query per row. All four are schema and query decisions the generator made once, and all four scale with rows or concurrency in a way a demo account never exercises.

A slow query and a status-page outage produce the identical symptom in your browser tab, and only one of them is Supabase’s problem to fix.

Supabase max connections and connection pool

Every project caps direct Postgres connections by compute tier: 60 on the free Nano tier and on Micro, 90 on Small, 120 on Medium, climbing through the dedicated tiers above that. A serverless function that opens a fresh direct connection per invocation burns through that cap the moment concurrent requests climb into double digits, and every request queued behind it reads as slow even though no query itself did anything wrong.

The fix is routing through the pooler, port 6543 in transaction mode, instead of the direct connection on port 5432. Supabase’s own connection management guidance gives a specific split for sizing the pool itself: keep it under 40% of your database’s max connections if you lean heavily on the PostgREST API, otherwise up to 80% is fine, with the rest held back for Auth and the platform’s own services. That one setting, pooled versus direct, is the fix behind a lot of what gets reported as “Supabase is slow.”

The index Postgres won’t add for you

Postgres indexes a foreign key’s target automatically, the primary key it points at, but not the column doing the pointing. The documentation says so directly: a foreign key declaration “does not automatically create an index on the referencing columns,” because indexing choices vary too much for Postgres to assume on your behalf. Migration after migration in the audit corpus wrote the constraint and stopped there.

-- Generated: a foreign key, no index on the referencing column.
alter table orders add column customer_id uuid references customers(id);

-- Add this yourself. Postgres never will.
create index on orders (customer_id);

The missing index is invisible at fifty rows, and it’s usually the whole difference between a query that felt instant in development and the same query crawling the day real customers start filling the table.

select(*) and the N+1 your ORM hides

An unbounded select fetches every column and every row with no limit. An N+1 fetches a list once, then fetches one related row per item in it, ten items, eleven round trips. Both read as clean code, and both are why an ORM sitting on top of Supabase can make things worse rather than better: a relation-loading convenience method looks like one call and quietly issues one query per row underneath it.

I audited a golf-shot analytics app built on Supabase (Vite, React, the browser client, row-level security correctly enforced) where the stats page fetched a user’s entire shot history with no limit and no pagination, then recomputed every average in the browser after it arrived. Fine for the ten shots in a demo account. For a real user with tens of thousands of logged shots, it’s a memory and CPU cliff on every page load, and the same app shipped a 434 KB demo dataset bundled to every visitor on top of it, a fixed cost nobody had measured.

// Fetches every column and every row. Fine at ten rows, not at ten thousand.
const { data } = await supabase.from('shots').select('*').eq('user_id', userId);

// Ask for what the page renders, with a limit and a cursor.
const { data } = await supabase
  .from('shots')
  .select('club, distance, created_at')
  .eq('user_id', userId)
  .order('created_at', { ascending: false })
  .limit(50);

What more compute buys, and what it doesn’t

Every compute tier raises the two numbers people assume are the whole problem: the connection ceiling from the tier ladder above, and raw horsepower. Supabase’s own compute guidance puts the decision plainly: check whether a bottleneck is hardware-constrained or software-constrained before paying for more hardware, because a missing index or an N+1 loop runs exactly as slow on a bigger instance.

A bigger compute tier buys you
What it never fixes
A higher direct-connection ceiling, 60 on Nano and Micro up through 500 on the largest dedicated tiers
A serverless function still opening one direct connection per request with no pooler
More CPU and RAM to run an expensive query faster
A missing foreign-key index turning a full table scan into an indexed lookup
Fewer timeouts under a traffic spike
An N+1 loop, which still runs one query per row no matter the hardware underneath it
A bigger compute tier buys you
A higher direct-connection ceiling, 60 on Nano and Micro up through 500 on the largest dedicated tiers
More CPU and RAM to run an expensive query faster
Fewer timeouts under a traffic spike
What it never fixes
A higher direct-connection ceiling, 60 on Nano and Micro up through 500 on the largest dedicated tiers
A serverless function still opening one direct connection per request with no pooler
More CPU and RAM to run an expensive query faster
A missing foreign-key index turning a full table scan into an indexed lookup
Fewer timeouts under a traffic spike
An N+1 loop, which still runs one query per row no matter the hardware underneath it

Upgrade compute once a load test shows the bottleneck is hardware, not a query. Upgrading before that test buys a faster machine for the same missing index. Connection ceilings are the dullest of the costs that arrive with the default, and why Supabase became the default for AI-built apps walks the rest of them. And if the conclusion you are reaching is that the platform itself has to go, read before you switch backends over speed first; the four causes above move with you.

Why images and files load slower than your queries

A slow dashboard and a slow image gallery usually trace to different systems entirely. Supabase Storage sits behind its own CDN, and the first request for a file from a given region is a cache miss that has to reach the origin server before later requests from that region are served from cache instead. A private bucket makes this worse by design, because each request re-checks per-user permissions before the cache can answer it, so two different users requesting the identical file both pay the cache-miss cost every time.

Image transformation compounds it further. Resizing happens on request, and an unresized original is usually the largest share of a page’s total weight. Serve images through the transformation endpoint at the size the layout actually needs, set a real cache-control header, and keep a bucket public unless a specific file needs a per-user permission check.

Fix the connection pool first; it’s the cheapest change with the widest blast radius, and load is one of the seven questions a launch turns on. The same rushed schema pass that skips an index tends to skip other things too, an unguarded cascade on the same table, for instance, which is a data-loss risk rather than a performance one; the data-loss bugs hiding in an AI-built app covers that half.

If the bottleneck you’re chasing is concurrency specifically rather than one slow query, why your AI app stalls at 100 concurrent users covers the connection-cliff mechanics in more depth. This post is about diagnosing which of the two you actually have.

Common questions about Supabase performance

Should I use an ORM with Supabase?

Prisma and Drizzle both work fine against a Supabase Postgres instance, and the connection string is the only thing that changes: point either one at the pooler, not the direct connection, or a serverless deployment reproduces the exact connection cliff above. The risk worth watching is a relation-loading convenience method that generates an N+1 without ever showing you a raw query. Read the generated SQL once, with EXPLAIN ANALYZE, before trusting an ORM method that touches more than one table.

Is Supabase down right now?

Check the status page rather than guessing from your own dashboard. It reports platform-wide incidents by region and service, Database, Auth, Storage, Realtime, and it’s the fastest way to rule the platform out before spending an hour rereading your own queries.

Do I need to upgrade my Supabase plan to fix slow queries?

Rarely, and not first. Run the load drill: route through the pooler, add the missing index, cap the unbounded selects, then measure again. The 21-app audit record turned up the same four wiring gaps regardless of plan tier, and a bigger instance runs the identical missing index at the identical speed, just for a larger bill.

What’s the fastest thing to check first?

The status page, then your connection string, in that order. Platform first, because it settles the question immediately. Then whether requests route through the pooler on port 6543 or a direct connection on port 5432, a setting a lot of “Supabase is slow” reports trace back to.