How to rate limit the Supabase API
Supabase limits only its Auth endpoints by default, so you add the other limits yourself: tune the Auth rate limits, turn on CAPTCHA, and count requests per user in your Edge Functions and database. A browser can call your tables and functions directly with the public anon key. Nothing slows a script that does the same.
This guide shows which limits exist, which ones you can change, and where to add your own. It uses short code examples that you can copy and test.
Does Supabase rate limit the API?
Supabase rate limits its Auth endpoints. It also has safeguards against bursts of traffic on the platform. The Supabase going-into-production guide says that the platform “employs a number of safeguards against bursts of incoming traffic to prevent abuse.”
The documentation that this article links does not describe a per-user limit for your tables or your functions. For this reason, you must assume that a caller with a valid key can send many requests. The caller can read rows, insert rows, and start your functions as often as the platform allows.
Rate limiting is not the first control. Read this order:
- Access control. RLS policies decide who may read or change each row. Without them, a limit only slows the attacker down. See the Supabase RLS guide.
- Rate limits. They reduce the harm from one caller who sends too many requests.
- Cost limits. They cap the money that a paid API or a function can cost.
The anon key is public in every Supabase app, as the anon key guide explains. So you cannot limit who holds the key. You limit the actions that the key can start.
Do not wait for an attack to start. Set the limits before launch, because a limit that you add during an attack costs more time. Write down the limit for each route. Then you can see which routes have no limit.
What abuse does a rate limit stop?
A rate limit helps against these problems:
- Sign-up floods. A script creates thousands of accounts and fills your user table and your email quota.
- Password guessing. A script tries many passwords for one email address.
- Email and SMS cost. A script asks for magic links or one-time codes without end.
- Paid API cost. A script calls an Edge Function that calls an LLM or an image API.
- Row flooding. A signed-in user inserts millions of rows, or posts spam in a loop.
- Scraping. A script reads every public row page by page.
Each problem needs a different limit. A limit on sign-in does not stop row flooding. The next sections go through them in the order of risk.
How do I change the Auth rate limits?
Open the dashboard and go to Authentication, then Rate Limits. The Supabase Auth rate limits page lists the default values and says which ones you can change.
The page lists these defaults. Read the page again before you rely on a number, because Supabase can change them:
| Operation | Default limit | Configurable |
|---|---|---|
| Sign-ups, magic links, OTP, recover, resend (by IP) | 30 requests per 5 minutes | Yes |
| Token requests: password, refresh, ID token, PKCE (by IP) | 150 requests per 5 minutes | Yes |
| Verification requests (by IP) | 30 requests per 5 minutes | Yes |
| New request for OTP, sign-up confirmation, or recover (by user) | One per 60 seconds | Yes |
| Anonymous sign-ins (by IP) | 30 requests per hour | Yes |
| MFA challenge and verify (by IP) | 15 requests per minute | No |
| Emails from the built-in provider | 2 per hour, for the whole project | Yes, with custom SMTP |
These limits protect your Auth service. They do not protect your tables, and they do not protect your own functions.
Use these steps:
- Lower the limits that your app does not need at the default value. A small app that has few users per IP address can use a lower sign-up limit.
- Set up custom SMTP for production. The built-in email provider has a project-wide limit that blocks real users when one attacker uses it up. A custom SMTP provider also lets you raise the email limit.
- Turn off sign-in methods that you do not use. If the app does not need anonymous sign-ins, turn them off. The Supabase anonymous sign-in guide warns that attackers can misuse the endpoint and make the user table much larger.
- Check the per-user and per-IP limits together. An attacker who changes IP addresses avoids the IP limits. The per-user limits still slow the attack on one email address.
You can also set the limits with the Management API. The Auth rate limits page names the rate_limit_* keys and the endpoint PATCH /v1/projects/{ref}/config/auth. Use a personal access token for this call, and keep the token private.
How do I add CAPTCHA to sign-up and login?
CAPTCHA asks a visitor to prove that a person is present. It stops simple scripts before they reach the Auth limits. Supabase supports hCaptcha and Cloudflare Turnstile, as the CAPTCHA documentation states.
- Create a site key and a secret key in the dashboard of your CAPTCHA provider.
- Open the Supabase dashboard. Go to the Auth section of the project settings.
- Under Bot and Abuse Protection, turn on CAPTCHA protection.
- Select the provider and enter the secret key. Save the change.
- Add the CAPTCHA widget to your sign-up, sign-in, and password reset forms.
- Pass the token to the Supabase call, as in this example:
const { data, error } = await supabase.auth.signUp({
email,
password,
options: { captchaToken },
})
Caution: After you turn on CAPTCHA, every client that calls the protected endpoints must send a token. Test the mobile app and any scripts before you deploy. A client without a token fails.
The anonymous sign-in guide also recommends CAPTCHA for anonymous users.
How do I rate limit my own Edge Functions?
Count the calls per user in a store, and refuse the call when the count is too high. The Supabase rate limiting guide for Edge Functions uses Upstash Redis for this. It says that the Redis client of Upstash works over HTTP, so it fits serverless code, and that you can key the limit to the user ID from Supabase Auth.
First create a Redis database at Upstash. Then store the two values as secrets. The names come from the Upstash rate limit documentation:
supabase secrets set UPSTASH_REDIS_REST_URL=your-url UPSTASH_REDIS_REST_TOKEN=your-token
Then write the function. This example checks the user first. It then limits the user to 10 calls in 60 seconds:
import { createClient } from "npm:@supabase/supabase-js@2";
import { Ratelimit } from "npm:@upstash/ratelimit@2";
import { Redis } from "npm:@upstash/redis@1";
const ratelimit = new Ratelimit({
redis: Redis.fromEnv(),
limiter: Ratelimit.slidingWindow(10, "60 s"),
});
Deno.serve(async (req) => {
const supabase = createClient(
Deno.env.get("SUPABASE_URL")!,
Deno.env.get("SUPABASE_ANON_KEY")!,
{ global: { headers: { Authorization: req.headers.get("Authorization") ?? "" } } },
);
const { data: { user } } = await supabase.auth.getUser();
if (!user) return new Response("Unauthorized", { status: 401 });
const { success } = await ratelimit.limit(user.id);
if (!success) return new Response("Too many requests", { status: 429 });
// Do the expensive work here.
return new Response("OK");
});
The check of the user comes first. The key for the limit is the user ID from a verified token. A caller cannot choose this value.
Return the status code 429 for a refused call. Your front end can then show a clear message.
Note: Pick the key with care. The user ID is the best key for signed-in routes. For public routes, use the IP address. The IP address is a weaker key, because a script can change it.
For an AI endpoint, add a second limit on cost. See the API abuse protection guide for more controls such as input size limits and fixed model settings.
How do I rate limit writes in Postgres?
Add a check to the RLS policy for insert. The policy counts the rows that the user created in the last hour. It refuses the new row if the count is too high. This works for the direct calls from the browser to the Data API, where no function runs.
This example limits each user to 20 posts per hour. It assumes a posts table with user_id and created_at columns:
create policy "limit posts per hour"
on public.posts
as restrictive
for insert
to authenticated
with check (
(
select count(*)
from public.posts p
where p.user_id = (select auth.uid())
and p.created_at > now() - interval '1 hour'
) < 20
);
The key word restrictive makes this policy an extra condition. A new row must pass this policy and pass your normal insert policy. Add an index on (user_id, created_at), so that the count stays fast.
There are limits to this method:
- It limits rows that the user creates. It does not limit reads.
- It runs a query on every insert. Use it for low-volume actions such as posts, comments, and orders.
- A user who deletes old rows can lower the count. Count in a separate log table that the user cannot change if this matters.
- The error message is a plain RLS error. Your front end must handle it.
For read limits, reduce what a call can return. Set a maximum page size and select only the columns that the page needs. Do not allow one call to return a whole table.
Can I put a gateway in front of Supabase?
Yes. A gateway or a web application firewall can limit requests by IP address before they reach your project. Examples are Cloudflare, a reverse proxy on your own server, or an API gateway service. This works best when your own domain receives the browser calls and the gateway forwards them.
Use this method with these cautions:
- The Supabase URL stays public. An attacker who knows the project URL can call it and skip the gateway. A gateway therefore reduces the traffic from normal clients only.
- A gateway sees the IP address, but it may not see the user ID inside the JWT. Per-user limits are easier in your own function.
- You must keep the gateway rules up to date, and you must test them.
For most small apps, the controls in the previous sections give more value for less work. Add a gateway when you have real traffic and a team that can run it.
How do I check that the limits work?
Test from outside the app. Use a terminal and a test account.
- Test the function limit. Call the function 15 times in a row with a valid user token. The first 10 calls must return
200. The next calls must return429.
for i in $(seq 1 15); do
curl -s -o /dev/null -w "%{http_code}\n" -X POST \
"https://YOUR_PROJECT.supabase.co/functions/v1/your-function" \
-H "Authorization: Bearer USER_JWT" \
-H "apikey: YOUR_ANON_KEY"
done
- Test the anonymous call. Call the function with only the anon key. It must return
401. - Test the insert limit. Insert 21 rows as one test user. The 21st insert must fail.
- Test sign-up with a test script. Send many sign-up requests from one IP address. The response must change to a
429status after the limit. Use a test project, not production. - Read the logs. Open Logs in the dashboard and look for Auth, API Gateway, and Edge Function events. Check that the refused calls appear and that normal users are not refused.
Do the tests again after each change to a limit. Then add an alert for a high count of 429 responses. A sudden rise can mean an attack. It can also mean that your limit is too low for real users.
When should I get help?
Get help in these cases:
- The traffic is large and your project is slow or down. Contact Supabase support, and give them the project ID and the time range.
- You see a high cost from a paid API and you do not know which route caused it. Delete the key first, and then find the route.
- Attackers change IP addresses and user accounts faster than your limits react. Ask a security engineer to add bot detection and account-level controls.
- You expect a launch with high traffic. The going-into-production guide says that Team and Enterprise customers who expect high load can open a support ticket in advance.
Common questions
Does Supabase rate limit my API by default?
Can I rate limit by user ID instead of IP address?
Is a rate limit the same as access control?
Can the anon key be rate limited?
Do I need Redis for a rate limit?
Test your abuse limits
A VibeEval scan sends repeated requests to your sign-up, login, and function endpoints and reports the endpoints that never refuse the caller.
14-day free trial · No credit card · Cancel anytime