Supabase RPC calls are slow by default, and the documentation won't tell you why.

Most people building with Supabase set up a few stored functions and call them from their frontend using supabase.rpc(). It works fine until it doesn't. You'll notice latency spikes, timeout errors on bulk operations, and general slowness that makes no sense given how small the data is. The issue isn't Supabase. It's how the RPC is structured, how PostgREST handles it, and what's happening at the database level that most guides skip entirely. Supabase RPC is just a PostgreSQL function exposed through PostgREST. When you call supabase.rpc('my_function', { params }), PostgREST wraps that in an HTTP POST request to /rpc/my_function with your parameters as JSON. PostgREST then invokes the database function, runs it inside a transaction, and returns the result. That round-trip path is where most performance gets leaked. There are connection setup costs, serialization overhead, and transaction wrapping that add up if you're not aware of them. One thing beginners consistently miss is that PostgREST creates a new database session per RPC call by default unless you're using their connection pooling layer properly. That means every single RPC invocation can incur a small connection handshake cost on top of your actual query execution time. Over thousands of calls, this adds up to noticeable lag.

Unlock Lightningfast Performance The Ultimate Guide To Supabase Rpc Optimization

Start with the function definition itself. The biggest factor in RPC speed is what happens inside the function, not the transport layer. Use plain SQL where possible instead of PL/pgSQL. A function defined as LANGUAGE sql executes inline and can be inlined by the PostgreSQL planner. A PL/pgSQL function runs in its own context with additional overhead. I had a client whose authentication check function was written in PL/pgSQL and took 80 milliseconds per call. Rewriting it as a pure SQL function dropped that to about 3 milliseconds. The logic was identical. The difference was purely in how Postgres executed it. Here's a practical example. Instead of this: Pl/pgSQL approach (slow):

CREATE OR REPLACE FUNCTION get_user_orders(user_id bigint) RETURNS SETOF orders AS $$ BEGIN RETURN QUERY SELECT * FROM orders WHERE user_id = get_user_orders.user_id; END; $$ LANGUAGE plpgsql; Use this: Pure SQL approach (fast):

Get the Full Details

Supabase for New Developers and Non-Developers: A Guide to Getting ...
Supabase for New Developers and Non-Developers: A Guide to Getting ...

CREATE OR REPLACE FUNCTION get_user_orders(user_id bigint) RETURNS SETOF orders AS $$ SELECT * FROM orders WHERE user_id = $1; $$ LANGUAGE sql; The second version is faster because Postgres can optimize it alongside the query planner rather than treating it as a black box. You also lose the transaction isolation overhead that PL/pgSQL introduces. Indexing matters more than you'd think here. If your RPC function queries a table without proper indexes on the filtered columns, Postgres will do sequential scans. I worked on a project where an RPC call that looked trivial — filtering a transactions table by user_id and date range — was taking 4 seconds because the table had 2 million rows and no composite index on those columns. Adding a single composite index cut the execution time to under 50 milliseconds. The function itself didn't change at all.

Another overlooked detail is returning the right type from your function. If you define your function as RETURNS json instead of RETURNS SETOF your_table, Postgres has to serialize each row into JSON internally. That's an extra processing step for every row returned. For large result sets, this serialization overhead can double your function's execution time. Use RETURNS TABLE or RETURNS SETOF wherever possible. Connection pooling is another area where people leave performance on the table. Supabase offers connection pooling through Supavisor or PgBouncer depending on your plan. If you're making many small RPC calls in quick succession from a frontend, the overhead of establishing connections repeatedly will hurt. Enabling transaction-level pooling or connection multiplexing can dramatically reduce latency for high-frequency RPC patterns. Here's a specific edge case I ran into recently. I was building a feature that called an RPC function in a loop from the client side — essentially firing off multiple independent requests sequentially. Each call returned in about 200 milliseconds, but the total page load time was over 6 seconds because the calls were serialized. The fix wasn't optimizing the function. It was rewriting it to accept an array parameter and process all the IDs in a single call using a JOIN instead of multiple individual queries. That brought the total time down to around 300 milliseconds for the entire batch. The lesson is that minimizing the number of RPC calls matters more than optimizing any single call.

Caching is also worth considering. PostgREST has built-in ETag-based caching for GET requests, but RPC calls are POST requests by default and don't benefit from the same caching behavior. If your RPC function is idempotent and returns the same data for the same inputs, consider restructuring it as a parameterized view or a GET endpoint that PostgREST can cache natively. This is a significant architecture tradeoff though, so only do it if the performance gain justifies the structural change. The final thing to check is your database-level settings. Settings like effective_cache_size, shared_buffers, and work_mem directly affect how fast your functions execute. On a fresh Supabase project, these are set to conservative defaults. If you're hitting consistent latency issues and your data volume has grown, checking your database statistics with pg_stat_statements can show you exactly which queries are costing you the most time. Look for high mean_exec_time values in your function calls and work backward from there. Also keep in mind that Supabase RPC has a hardcoded response size limit. If your function returns a massive result set, you'll hit the 100MB response cap and the call will fail entirely. This isn't a performance issue per se, but it's a hard ceiling that will blindside people who aren't expecting it. Implement pagination or filtering at the function level to keep response sizes reasonable.

How To Install Supabase CLI [NEW 2026 Guide] - YouTube
How To Install Supabase CLI [NEW 2026 Guide] - YouTube

If you need extremely low-latency RPC calls at scale, the honest answer is that PostgREST may not be the right tool. Some teams have moved heavy real-time operations to raw PostgreSQL connections or dedicated microservices that bypass PostgREST entirely. It's a bigger architectural shift, but it's worth considering if your RPC call counts are in the tens of thousands per minute and latency is below 10 milliseconds.