Performance issues with RLS
Hi,
I am quite new with working with RLS, while I do have a background with Postgres I am having a hard time making queries run fast with RLS. I have to admit I have quite a complex app, but perhaps RLS is not meant to be as fast as I expected it to be.
I for example have a function get_users_paginated, it gets 20 users at the same time, when having the RPC as invoker it took around 200ms on a loaded table 100k records. Doing some explain analyze showed me that each row was checking the invoking user context (cant do JWT for roles and stuff). When making a very fast function that does caching within the transaction I got it down to 60ms. However when making the RPC DEFINER and checking upfront I get like 11ms. Is this 49ms slower just a accepted cost of RLS. Or am I doing something wrong?
26 Replies
Please check the performance guide here: https://supabase.com/docs/guides/database/postgres/row-level-security#rls-performance-recommendations
In particular you don't seem to be wrapping your functions in selects.
Also if you can use security definer functions with filters to avoid RLS on sub tables if that can be done helps.
If you skim thru that doc you can see RLS can make huge performance impacts if not done correctly (or in some cases may have to be reworked).
Hmm I did forget some select in this function, will messure if that makes a difference, since the function it calls is already heavily cached.
It will end up running the function on each row versus caching the result to compare.
Ah so security definer RPCs with filters is accepted, and expected to have better performance. Would you say 49ms is a expected cost of RLS, ofcourse accepted depends on the app.
It makes a difference even on auth.uid() which is just looking at a JWT.
Yeah true, will change this and messure again
Look at some of the times in the doc. RLS can be seconds of added time or hardly any at all.
No easy way to just say what it will be.
Some things may not help also depending on indexing, but rarely does anything in the guide hurt times.
Yes true, a simple policy that just checks against a claim was instant for me. Thats why I put that one on top since its 95% of the case and postgres circuit breaks
or statementsYou are not guaranteed they will run in "order"
Its quite well indexed, I notice that planning time is sometimes 2x the execution time. Since some policies get really complex.
Ah forgot about that. In my testing cases it happened to be in order according to the path from analyze.
It is all up to what the analyzer decides.
I also inlined most of the policies, just in case the functions are black boxes. So in most cases the missing select is not a issue since I dont call functions (only call functions when I get a infinte recursion). And with the inlined policies I also have like 3x slower queries.
If you can write your functions you use in RLS so that they are secure as a security definer (or put them in a schema that is safe) then that can be a big win. But you have to make sure you don' t depend on the table they are accessing RLS.
And everything is really well indexes, no seq scans
Inlining can be much slower than a well written function as shown in the minimize join section. Surrounded by a select.
Ahh okay thanks
Well I am going to play around with this some more. Thanks!!
AI's are not much help with this. RLS in postgres is not used in the past like it gets used in Supabase (PostgREST). So not much history.
Hey, I tried some of the stuff you told me and got the performance in simple cases to be 1ms which is a 10x compared to bypassrls. In more complex cases where someone is subject to groups and locations control, So they are in a group which is part of a subject in a location and therefore they have access, I got it down to 13ms which is 130x compared to bypass RLS. Would you say this is good and the accepted RLS cost. Or would you say even in these complex cases it can be lower.
The RLS one is twice as slow as using a definer function that checks permissions at the start and rreturns the query. I
When I do where id =... it is basically just 0.5ms slower, this is crazy haha
So tldr, performance with specific eq is perfect. but for things like EXPLAIN ANALYZE SELECT * FROM api.subjects LIMIT 40; I go from 6 ms with definer and checking to 13 with RLS and invoker.
(its indexed but due to the query and table size, the planner choose seq.)
If you have a column in a table and can use a filter that is always the way to go as far as I know. Don't rely on RLS to filter if the column is on the main table. https://supabase.com/docs/guides/database/postgres/row-level-security#add-filters-to-every-query
Anything with fetching the entire table (which includes order and limit) has to process the entire table in most cases.
I basically just use the guidelines and if a large table do a quick check. If I don't see in 100msec or larger queries I move on during dev. Odds are I will end up changing something before production and need to change the RLS anyway. The guidelines tend to elimate the 10x or 100x cases and then you are debating over 2x on small numbers.
If you know you will have a large table though it is worth checking out the performance in more detail. There are cases like with using an array returning function that the method will break down if the number of results is above 1000. In that case may need to think about redoing the approach to data organization.
Here was a study I did on roles/permissions and showing some functions that work well. https://github.com/GaryAustin1/custom-properties
Yeah thanks, honestly this is fine in this stage. I will just monitor and improve where needed.
Thanks for helping
Perhaps a good rule would be that I use RPC with definer for things where I just select many things from a table. And RLS for specific where eq etc
Hey @garyaustin I assume functions that take a param for each row should not be called with select right?
You should avoid those at all costs for large table selects. The "wrapping" select won't do anything and they will run on each row.
If possible the guides show returning an array or list and comparing the row column to that.
Hey @garyaustin how are you?
I got a question, what do you think of patterns like these:
Considering that task_goals dont really have any columns you can determine access with but tasks does.
Do you think the select here is usefull considering that a task often has multiple goals that are selected at the same time and would evaluate the same task id
https://supabase.com/docs/guides/database/postgres/row-level-security#minimize-joins
You don't want task_goals.task_id being used in a query as then it has to run on every row if you can avoid it. The link shows using in (or any) to compare the main table column to a list/array which is faster. \
Row Level Security | Supabase Docs
Secure your data using Postgres Row Level Security.
What would you do for something like this:
Like I dont really have something to work with in task_goals
I use that most of the time but dont really see a way around it here
Access here is determined by creator id or chapter id
I don't have the brain power today (bad head/chest cold) to work RLS in my head today.
All I can say is you want to organize RLS so that you compare a main table row to a result (even 100's of values array). You never want to be using the main column in a select join or passing to a function. This is mainly for select RLS on large tables to have to worry about it.
All I can say is you want to organize RLS so that you compare a main table row to a result (even 100's of values array). You never want to be using the main column in a select join or passing to a function. This is mainly for select RLS on large tables to have to worry about it.
Ah I am sorry. Thanks I will see what I can do with that!