Is Supabase slow? Rarely on its own: it runs ordinary Postgres. When a Supabase app drags, the cause is usually one of five things: a query with no index, a row level security policy that runs once per row, one request per list item, too many direct connections, or the smallest compute size. Start with the Query Performance report.
Is Supabase slow? The five causes, and the control behind each
A slow Supabase app usually has one of five causes: a query that reads the whole table, a row level security policy checked once per row, one request per list item, more direct connections than the database accepts, or a compute size too small for the load. Each cause has its own first check.
When Supabase is slow for every user at once, check the platform first; when one screen is slow, check that screen’s query. Supabase speed on a correctly sized project comes down to Postgres speed, so the same habits that make web performance optimization for an AI-built app work apply here. In our June and July 2026 audits, the Performance & Scale pillar was scored on 21 third-party apps and averaged 53.3 out of 100, ninth of twelve pillars counting from the weakest. We chose those apps, so read the figure as a description of that group, not a random sample and not a rate for Supabase apps.
| Cause | What you see | The first check | The control |
|---|---|---|---|
| A query that reads the whole table | One list or search page is slow, and slower as rows grow | The Query Performance report, then that statement’s plan | An index on the columns the query filters, joins and sorts on, foreign keys included |
| A per-row policy check | Fast when run as postgres, slow for signed-in users | The plan of the same query run as a signed-in user | auth.uid() wrapped in a select, and the policy column indexed |
| One request per list item | One list view fires dozens of requests in the browser’s Network panel | Count requests per screen | One query with a nested select, or an .in() filter |
| More direct connections than the database accepts | Connection errors under traffic from serverless functions | Connections in the Database report | The shared pooler in transaction mode, port 6543 |
| A compute size too small for the measured load | High swap, a spent Disk IO budget, every query slower by a similar amount | Database Health | More compute, after rows 1 to 4 are fixed |
The Supabase warnings, and what each one means
These are the strings people paste from the dashboard. Each one below is quoted, explained from Supabase’s docs where they cover it, and paired with the first thing to check.
Two errors need no section of their own. canceling statement due to statement timeout is Postgres error 57014, query_canceled, raised because the statement ran past its role’s timeout; Supabase’s defaults are 3 seconds for anon and 8 seconds for authenticated. Raising the timeout only hides the slow query, so find and fix the query first. Connection-limit errors such as “remaining connection slots are reserved” and “sorry, too many clients already” each have an entry in every too-many-connections error and its fix.
Your project is currently exhausting multiple resources
The exhausting-multiple-resources banner means the project is short on more than one resource at once. Which resources it counts, and the threshold that raises it, are not stated in Supabase’s docs. The banner offers two ways out, more compute or finding the most expensive queries, and the second one is the place to start.
The full text reads “Your project is currently exhausting multiple resources, and its performance is affected” and asks you to “upgrade your compute or use the AI assistant to identify and optimize the most expensive queries”. To see which resource is short, open Database Health in the Observability section, where Supabase sends you to check both RAM and the Disk IO budget. Then go to the Query Performance report, covered in the fix section below, before paying for a larger size.
Your project is about to deplete its Disk IO budget
The Disk IO budget is a daily allowance that lets compute sizes up to 2XL burst above their baseline disk performance. Once it is used up, the instance reverts to its baseline: response times can increase noticeably, CPU rises on IO wait, and daily backups and autovacuum can be disrupted.
That warning is the early notice: the budget is close to spent but not yet gone. You watch the Supabase Disk IO budget in Database Health. Supabase’s guide to high disk IO lists the usual causes: high memory use pushing pages to swap (every project has 1 GB of disk set aside for swapping), a low cache hit rate that sends reads straight to disk, queries that take more than a second, and more requests than the instance can handle. It adds that larger compute options, 4XL and above, have more consistent disk performance. A budget that is spent every day after the slow queries are fixed is the measured case for more compute, covered under the compute step below.
Supabase out of memory: high RAM and swap
Supabase out of memory is read from swap in Database Health, not from the RAM figure. Supabase’s guide says that if swap is above 70%, chances are high the compute hardware is not suitable for the workload, and it lists slow queries, open connections and memory-hungry extensions as the common causes.
A high RAM number alone can be normal: Supabase’s guide to high RAM usage says a 1 GB instance can sit at a base memory usage of about 50%, because Supabase needs a range of services besides Postgres to operate. The effects it lists are degraded performance while the instance uses swap, the operating system killing processes as memory runs out and, in rare cases, an unresponsive instance. Its two fixes are a Compute Add-on or optimizing what the database does. One cause links this warning to the connection step: every connection to the database consumes memory, so a flood of direct connections shows up here too.
How to fix slow Supabase queries, in order
The order below works through Supabase slow queries from the cheapest check to the most expensive change, and each step names how to tell whether it applies. The steps follow Supabase’s and PostgreSQL’s documentation, quoted where a limit, a port or a default is involved.
Rule out the platform, the region and a paused project
Start with Supabase’s status page, which lists components such as Database, Auth, Storage, Realtime and the Connection Pooler, with regions shown under Compute capacity. A posted incident that matches your product and region is your answer; an empty list only means nothing has been posted.
Distance comes next. Supabase’s region guidance says to choose the location closest to your users for the best performance, since every round trip pays the network latency between the user and the region. Time the slow request from a machine near your users with curl’s -w option:
curl -s -o /dev/null \
-w "connect %{time_connect}s first byte %{time_starttransfer}s total %{time_total}s\n" \
-H "apikey: $ANON_KEY" -H "Authorization: Bearer $USER_JWT" \
"https://<project-ref>.supabase.co/rest/v1/orders?select=id,total&limit=20"
curl’s manual defines time_connect as the time until the TCP connect completed, and time_starttransfer as the time until the first byte arrived, including the time the server needed to calculate the result. A large connect time points at distance; a large gap between connect and first byte points at the server, meaning the query, a policy or the compute size. A request that is fast in your own test and slow for your users points at distance or at a per-user policy. A region is not a setting you can change: how Supabase moves a project to another region is a new project in the target region plus a migration, with keys and the API URL updated.
On the Free plan, check that the project is running at all. Supabase’s project pausing page says a Free plan project counts as inactive if it does not receive sufficient user database activity over the past week. A paused project stays paused until someone restores it, with Resume project in the dashboard or the Management API’s restore endpoint.
Find the slow query: Supabase query performance and pg_stat_statements
The Query Performance report is the first stop for a slow Supabase query. Supabase describes it as a detailed analysis of slow database queries, reached from the Database report in the dashboard’s Reports, and it needs no code. The statement to fix first is usually the one with the most total time across all its calls.
Open Reports, then the Database report, and follow its link to the Query Performance report. The same data is in SQL: Supabase’s pg_stat_statements page describes the extension as tracking planning and execution statistics of all SQL statements executed on the database. This lists the ten statements with the most total execution time:
select calls,
round(mean_exec_time::numeric, 1) as mean_ms,
round(total_exec_time::numeric) as total_ms,
query
from pg_stat_statements
order by total_exec_time desc
limit 10;
In the PostgreSQL manual, calls is the number of times the statement was executed and total_exec_time the total time spent executing it, in milliseconds. A statement with a modest mean and a huge call count often costs more than the one slow report a user complained about. Next, read its plan. EXPLAIN ANALYZE actually executes the statement, so wrap a write in BEGIN and ROLLBACK, as the PostgreSQL manual shows, or run it on a copy of the data. Supabase can return plans through the client with explain(), but keeps it disabled by default to protect sensitive information about your database structure. Reading the plan itself is covered in how to analyze a Postgres query plan. Save the plan and the timing: that pair is the “before” half of the proof in the verify section.
Index the foreign keys and the columns your filters use
Postgres does not index the referencing side of a foreign key for you. The manual says “the declaration of a foreign key constraint does not automatically create an index on the referencing columns”, while noting that it is often a good idea to index them, because a delete from the referenced table, or an update of a referenced column, requires a scan of the referencing table. Supabase flags these in the Performance Advisor with its 0001_unindexed_foreign_keys lint, “Unindexed foreign keys”, warning that queries which filter or join on those columns will be slower.
For a specific slow statement, the index_advisor extension takes the query as text and returns suggested index_statements with costs before and after, though it only recommends single-column B-tree indexes. Index the columns the slow statement filters, joins and sorts on, not every column in the table:
-- orders.customer_id references customers.id; Postgres did not index it
create index concurrently orders_customer_id_idx
on public.orders (customer_id);
concurrently builds the index without locks that prevent concurrent inserts, updates or deletes, where a standard build locks out writes until it finishes; it cannot run inside a transaction block, so run it as a statement on its own. Then run the plan again and compare it with the one you saved.
Wrap auth.uid() in a select: the per-row policy cost
A policy written as auth.uid() = user_id can call the function for each row it checks. Supabase’s RLS performance guide recommends wrapping the function in a select, which makes the optimizer run an initPlan and “cache” the result instead of calling it on each row, and indexing columns used in policies that are not already primary keys or unique.
-- before: auth.uid() can be called once per row
create policy "own orders" on orders for select to authenticated using (auth.uid() = user_id);
-- after: the optimizer evaluates it once and reuses the result
alter policy "own orders" on orders using ((select auth.uid()) = user_id);
The guide adds a condition: wrap a function only when its result does not change based on the row data. The symptom is a query that is fast in the SQL editor and slow for signed-in users: dashboard queries run as postgres, which has bypassrls on Supabase, so policies do not slow them. The guide’s own test runs the query with RLS on and then off, in a non-production environment only; if the times are similar, the query itself is likely the problem. The wider set of policy fixes, each measured, is in why a correct RLS policy still slows queries. A policy edit is also a security change, so sign in as a second user afterward and confirm they still see none of the first user’s rows.
One request per row: the N+1 pattern in a Supabase client
The N+1 pattern is a list fetched in one request, followed by one more request per item for its related rows. With 20 items that is 21 requests; once the list grows, the list page fires hundreds of queries for one screen. You can see it without any tool: open the browser’s Network panel, filter to your project’s supabase.co host, and load the page. A list view that adds a request for every row it renders has the pattern.
There are two fixes. A nested select, such as select('name, instruments(name)') in Supabase’s own reference, fetches the related rows in the same request. An .in() filter, which matches rows whose column is in an array of values, turns the per-item lookups into one request. The code for both, and the cases where a few extra requests are fine, are in fix the N+1 query problem in Supabase.
Return fewer rows and fewer columns
select('*') with no limit asks for every column of every row the policies allow. Supabase’s reference says projects return a maximum of 1,000 rows by default, a value set in Project API Settings, so a list can be capped and slow at the same time. Ask for the columns the screen shows, and page with .range(), which returns only the records between a from and a to offset. Large rows sent to every visitor also count against your bandwidth, which is covered in what is eating your Supabase bandwidth.
Serverless functions and the connection limit
Code that uses the Data API through supabase-js works over REST, so it needs no Postgres client and opens no connections of its own. Code that opens a connection string does: an ORM, a long-running server or a serverless function. Each compute size has a recommended maximum of database connections, 60 on the Nano and Micro sizes, and Supabase’s own services, such as Auth and Storage, use some of them.
Supabase’s connection guide says to use the transaction mode connection string, the shared pooler on port 6543, for serverless and edge functions, which open many short-lived connections. Transaction mode does not support prepared statements, so turn them off in your connection library. Skip this and connection exhaustion can make the database unavailable during busy periods, which is how a traffic spike can turn into errors for every user. Sizing the pool is covered in connection pooling for Postgres, sized and tested.
When more compute is the fix, and when it only delays one
More compute fixes a project whose measured workload has outgrown its size: swap high, the Disk IO budget spent every day, CPU pinned after the queries are fixed. It does not fix a missing index or a per-row policy, which return on the bigger size as the data grows.
Free plan projects run on the Nano size, with shared CPU and up to 0.5 GB of memory, as Supabase’s compute and disk page lists. Supabase does not upgrade compute on its own, because a size change involves downtime, so plan the change. The price of each size is in what each Supabase compute size costs. A larger instance is vertical scaling, and the choice between a bigger database and more servers is in horizontal or vertical scaling. The order of changes as traffic grows is in what to change before you add a server. Moving to another host does not fix a missing index either, since the index is just as missing on any other Postgres host. If you still want to compare hosts, start with Supabase alternatives, compared on what you would rebuild.
Storage and Realtime are separate systems
Slow images are a matter of file size, cache status and dimensions, not SQL. All assets uploaded to Supabase Storage are cached on a CDN, and public buckets get a better cache hit rate than private ones, whose objects are checked for each user; the cf-cache-status header shows HIT or MISS. Supabase can also resize and optimize images on the fly, on the Pro Plan and above. The checks for slow images are in the image optimization checklist. Slow live updates are Realtime, with their own limits and their own diagnosis in Supabase Realtime that is slow or not working.
How to verify each fix: the same load, before and after
A Supabase fix is proven by the same measurement before and after it: the query plan and timing for an index, the query count for an N+1 change, connection usage for pooling, and latency and error rate for the whole workload at a set concurrency.
Take the “before” readings first, and take the “after” readings on the same data with the same load. Each check below names the evidence to keep.
- 01 Save the slow statement's plan and timing before the change and again after it. Expect the sequential scan to be gone and the time lower. Evidence: both plans.
- 02 Count database queries per request on the slow list view before and after an N+1 change, using the Network panel's request count or the statement's calls in pg_stat_statements. Evidence: the two counts.
- 03 Sign in as a normal user and run the policy-backed query again: record the time and the rows returned, then confirm a second user's rows still do not appear. Evidence: the timings and both row sets.
- 04 Send the same concurrent requests through the pooled connection string. Evidence: connection count, errors and the pool settings.
- 05 Run one load test of the main user journey at a fixed concurrency and duration, before and after, on a staging copy. Evidence: latency and error rate for both runs.
- 06 Watch Database Health for a week after the change. Evidence: swap, CPU and the Disk IO budget for that week.
Resetting the pg_stat_statements counters with pg_stat_statements_reset can only be done by superusers by default, so if your role cannot run it, compare two snapshots of the statement’s calls taken before and after. How to plan and run the test in check 5 is covered in load testing a web application; never point it at production without a plan.
The same checks are how four deliverables of the Production Hardening Sprint are proven. Deliverable 4.2 is verified this way: retain before-and-after query plans and timings; document the index decisions. For deliverable 4.3, the verify step is to measure database query counts for representative list and detail views. Deliverable 4.5 is verified this way: run concurrent requests and record connection usage, errors, and pool configuration. And for deliverable 9.2, the verify step is to report the workload, duration, environment, concurrency, latency, and error rate before and after changes.
Where the sprint does this
If the app has paying users and a launch or an investor review is close, and the fixes above reach across indexes, queries, connections and load, bring the Query Performance report to a free audit: a video call where we read it with you and agree which slowdown your users feel most. In the sprint, our engineers review every foreign key and hot query path with execution plans and implement appropriate indexes (4.2). We replace repeated per-record lookups with appropriate joins, batching, or prefetching (4.3). We configure connection pooling or the platform’s equivalent connection management and test it under load (4.5), and we simulate concurrent users, identify the first bottlenecks, fix them, and rerun the workload (9.2). Those are 4 of the 123 deliverables. Your app’s current framework and hosting setup are our starting point, and we refactor or replace components where the production work requires it.
Common questions about a slow Supabase project
Is Supabase having issues right now?
Supabase’s status page answers that: it lists current incidents by component, such as Database, Auth, Storage and Realtime, with regions under Compute capacity. If nothing matching your product and region is posted, look for Supabase issues in your own project next: time the slow request and open the Query Performance report.
Why is Supabase so slow?
Usually because of one of five causes: an unindexed query, a per-row policy, one request per list item, too many direct connections, or too little compute. On the Free plan two more apply: the Nano size, with shared CPU and up to 0.5 GB of memory, and a project paused after a week without enough database activity.
Can Supabase handle millions of users?
The platform is rarely the limit: Supabase lists compute sizes up to 16XL, with 256 GB of memory and 64 dedicated vCPUs, and custom sizes beyond that. Whether your project can handle a given load depends on your queries, policies and connections, and is a measurement: a load test at the traffic you expect, run after the queries and connections are fixed.
Which database is fastest?
No database is fastest in general. Every Supabase project runs its own Postgres instance, so a query that is slow for want of an index stays slow on another Postgres host until the index exists; speed comes from the query, the indexes and the compute size more than from the brand.
Is your Supabase app slow with real users?
Slow pages usually trace back to a few queries, and they get worse as more people sign up. On the audit, we go over how your app loads its data with you and name the likely causes. You leave knowing which slowdown to fix first.
Book a free audit →
Free · 30 minutes · No code access needed