Supabase rate limits fall into two categories: auth email limits (2 emails per hour on default SMTP) and API request limits (varies by plan). Fix email rate limits by configuring a custom SMTP provider like Resend or SendGrid in Dashboard > Authentication > Settings. Fix API rate limits by implementing client-side throttling, caching responses, and using connection pooling. Monitor your usage in Dashboard > Settings > Usage to stay within your plan's limits.
Understanding and Fixing Rate Limit Issues in Supabase
Supabase applies rate limits to protect infrastructure and prevent abuse. The most commonly encountered limit is the default SMTP rate limit of 2 auth emails per hour, which affects signups, password resets, and magic links. API request limits vary by plan and can be hit by high-traffic applications or inefficient client-side code. This tutorial explains each type of rate limit, how to identify which one you are hitting, and how to fix or work around each.
Prerequisites
- A Supabase project experiencing rate limit errors
- Access to the Supabase Dashboard
- Basic understanding of HTTP status codes (429 = rate limited)
Step-by-step guide
Identify which rate limit you are hitting
Identify which rate limit you are hitting
Supabase has several rate limits that produce different error messages. Check your error message or HTTP status code to determine which limit is triggered. The most common is the auth email rate limit, followed by API request limits. Check Dashboard > Logs > Auth or API Gateway to see the exact error messages from the server side.
1// Common rate limit error messages:23// 1. Auth email rate limit (default SMTP)4// Error: 'Email rate limit exceeded'5// HTTP 429 on /auth/v1/signup or /auth/v1/recover6// Cause: Default SMTP allows only 2 emails/hour78// 2. Auth signup rate limit9// Error: 'For security purposes, you can only request this once every 60 seconds'10// Cause: Same email requested too quickly1112// 3. API request rate limit13// Error: 'Too many requests'14// HTTP 429 on /rest/v1/* endpoints15// Cause: Exceeded plan's API request quota1617// 4. Realtime connection limit18// Error: 'Too many connections'19// Cause: Exceeded max concurrent WebSocket connections2021// Check which limit:22console.log('Status:', error?.status) // 429 = rate limited23console.log('Message:', error?.message)Expected result: You identify the specific rate limit that is causing errors in your application.
Fix email rate limits with custom SMTP
Fix email rate limits with custom SMTP
The default Supabase SMTP allows only 2 auth emails per hour. This is the most common rate limit issue. Fix it by configuring a custom SMTP provider in the Dashboard. Resend, SendGrid, and Mailgun all offer free tiers with generous limits. After configuring custom SMTP, email rate limits are governed by your SMTP provider, not Supabase.
1# Dashboard > Authentication > Settings > SMTP Settings2# Enable Custom SMTP: ON34# For Resend (recommended, free tier: 100 emails/day):5# Host: smtp.resend.com6# Port: 4657# Username: resend8# Password: re_your_api_key9# Sender email: noreply@yourdomain.com1011# For SendGrid (free tier: 100 emails/day):12# Host: smtp.sendgrid.net13# Port: 46514# Username: apikey15# Password: SG.your_api_key16# Sender email: noreply@yourdomain.com1718# For Mailgun:19# Host: smtp.mailgun.org20# Port: 46521# Username: your_mailgun_username22# Password: your_mailgun_password23# Sender email: noreply@yourdomain.com2425# After saving, test by triggering a signup or password resetExpected result: Auth emails are sent through your custom SMTP provider with no more 2/hour rate limit.
Implement client-side throttling for API requests
Implement client-side throttling for API requests
If you are hitting API request rate limits, the cause is usually inefficient client-side code making too many requests. Common patterns include: fetching data on every keystroke, re-fetching on every render, or polling without intervals. Implement debouncing for search inputs, caching for repeated queries, and proper useEffect dependencies to prevent unnecessary requests.
1import { createClient } from '@supabase/supabase-js'23const supabase = createClient(4 'https://your-project.supabase.co',5 'your-anon-key'6)78// BAD: Fetches on every keystroke9function handleSearch(query: string) {10 supabase.from('products').select('*').ilike('name', `%${query}%`)11}1213// GOOD: Debounced search (only fires after 300ms pause)14function debounce<T extends (...args: any[]) => any>(fn: T, delay: number) {15 let timer: ReturnType<typeof setTimeout>16 return (...args: Parameters<T>) => {17 clearTimeout(timer)18 timer = setTimeout(() => fn(...args), delay)19 }20}2122const debouncedSearch = debounce(async (query: string) => {23 const { data } = await supabase24 .from('products')25 .select('*')26 .ilike('name', `%${query}%`)27 .limit(20)28 // Update state with results29}, 300)3031// Use in input handler:32// <input onChange={(e) => debouncedSearch(e.target.value)} />Expected result: API requests are reduced by debouncing, caching, and limiting query results.
Add retry logic with exponential backoff
Add retry logic with exponential backoff
When you receive a 429 rate limit response, do not retry immediately. Implement exponential backoff that waits longer between each retry attempt. This prevents overwhelming the server with retry storms and gives the rate limit window time to reset. Most rate limits reset within 60 seconds.
1async function fetchWithRetry<T>(2 fn: () => Promise<{ data: T | null; error: any }>,3 maxRetries = 34): Promise<{ data: T | null; error: any }> {5 let lastError: any = null67 for (let attempt = 0; attempt <= maxRetries; attempt++) {8 const { data, error } = await fn()910 if (!error) {11 return { data, error: null }12 }1314 // Only retry on rate limit errors (429)15 if (error.status !== 429 || attempt === maxRetries) {16 return { data: null, error }17 }1819 lastError = error2021 // Exponential backoff: 1s, 2s, 4s22 const delay = Math.pow(2, attempt) * 100023 console.log(`Rate limited, retrying in ${delay}ms (attempt ${attempt + 1})`)24 await new Promise((resolve) => setTimeout(resolve, delay))25 }2627 return { data: null, error: lastError }28}2930// Usage:31const { data, error } = await fetchWithRetry(() =>32 supabase.from('todos').select('*')33)Expected result: Rate-limited requests are retried with increasing delays, giving the rate limit time to reset.
Monitor usage and plan for scaling
Monitor usage and plan for scaling
Check Dashboard > Settings > Usage to see your current API request counts, database size, storage usage, and bandwidth consumption relative to your plan limits. If you are consistently hitting limits, consider upgrading your plan or optimizing your queries. Set up monitoring to get alerted before you hit limits in production.
1# Dashboard > Settings > Usage2# Monitor these metrics:3# - API requests: total requests per month4# - Database size: storage used5# - Bandwidth: data transferred6# - Edge Function invocations: function calls7# - Realtime connections: concurrent WebSocket connections89# Plan limits (approximate):10# Free: 500K API requests/month, 500MB database, 1GB bandwidth11# Pro: Unlimited API requests, 8GB database, 50GB bandwidth12# Team: Unlimited API requests, 8GB database, 50GB bandwidth1314# If hitting limits on Free plan:15# 1. Optimize queries (add .limit(), use indexes)16# 2. Cache responses client-side17# 3. Upgrade to Pro ($25/month) for unlimited API requests1819# If hitting limits on Pro plan:20# Contact Supabase support for custom limits21# Or implement your own rate limiting with Edge FunctionsExpected result: You understand your current usage and can plan for scaling or optimization.
Complete working example
1// Supabase client wrapper with rate limit handling2import { createClient } from '@supabase/supabase-js'34const supabase = createClient(5 process.env.NEXT_PUBLIC_SUPABASE_URL!,6 process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!7)89// Retry helper with exponential backoff and jitter10async function withRetry<T>(11 operation: () => Promise<{ data: T | null; error: any }>,12 options: { maxRetries?: number; baseDelay?: number } = {}13): Promise<{ data: T | null; error: any }> {14 const { maxRetries = 3, baseDelay = 1000 } = options1516 for (let attempt = 0; attempt <= maxRetries; attempt++) {17 const result = await operation()1819 if (!result.error || result.error.status !== 429) {20 return result21 }2223 if (attempt < maxRetries) {24 const delay = baseDelay * Math.pow(2, attempt) + Math.random() * 100025 await new Promise((r) => setTimeout(r, delay))26 }27 }2829 return { data: null, error: { message: 'Rate limit exceeded after retries', status: 429 } }30}3132// Debounce helper for search inputs33function debounce<T extends (...args: any[]) => any>(fn: T, ms: number) {34 let timer: ReturnType<typeof setTimeout>35 return (...args: Parameters<T>) => {36 clearTimeout(timer)37 timer = setTimeout(() => fn(...args), ms)38 }39}4041// Simple in-memory cache42const cache = new Map<string, { data: any; timestamp: number }>()43const CACHE_TTL = 60_000 // 1 minute4445async function cachedQuery<T>(key: string, query: () => Promise<{ data: T | null; error: any }>) {46 const cached = cache.get(key)47 if (cached && Date.now() - cached.timestamp < CACHE_TTL) {48 return { data: cached.data as T, error: null }49 }5051 const result = await withRetry(query)52 if (result.data) {53 cache.set(key, { data: result.data, timestamp: Date.now() })54 }55 return result56}5758// Usage examples:5960// Cached + retry-safe query61const todos = await cachedQuery('todos-list', () =>62 supabase.from('todos').select('*').order('created_at', { ascending: false }).limit(50)63)6465// Debounced search66const searchProducts = debounce(async (query: string) => {67 return withRetry(() =>68 supabase.from('products').select('*').ilike('name', `%${query}%`).limit(20)69 )70}, 300)7172export { supabase, withRetry, debounce, cachedQuery, searchProducts }Common mistakes when fixing Rate Limit Issues in Supabase
Why it's a problem: Not configuring custom SMTP and hitting the 2 emails/hour limit repeatedly during development
How to avoid: Configure a custom SMTP provider (Resend, SendGrid, Mailgun) in Dashboard > Authentication > Settings > SMTP before you start development. This is the number one rate limit issue.
Why it's a problem: Retrying rate-limited requests immediately without backoff, making the problem worse
How to avoid: Implement exponential backoff with jitter. Wait at least 1 second before the first retry, doubling the wait time for each subsequent attempt.
Why it's a problem: Fetching data on every render or keystroke without debouncing or caching
How to avoid: Debounce search inputs (300ms delay), cache frequently accessed data, and use proper React dependency arrays to prevent unnecessary re-fetches.
Best practices
- Configure custom SMTP immediately when starting a new Supabase project to avoid the 2 emails/hour default limit
- Implement exponential backoff with jitter for all API calls to handle transient rate limits gracefully
- Debounce search inputs and other high-frequency API calls with a 300ms delay
- Cache frequently accessed data client-side with a short TTL to reduce redundant requests
- Add .limit() to all queries to fetch only the data you need
- Monitor usage in Dashboard > Settings > Usage to proactively identify when you are approaching limits
- Use connection pooling (Supavisor) for server-side applications making many concurrent database queries
Still stuck?
Copy one of these prompts to get a personalized, step-by-step explanation.
My Supabase app is getting 429 rate limit errors on auth email endpoints. I am hitting the 2 emails per hour SMTP limit. Walk me through configuring Resend as a custom SMTP provider step by step, including creating a Resend account, getting the API key, and configuring it in the Supabase Dashboard.
Create a Supabase client wrapper that handles rate limits automatically with exponential backoff, includes a debounce utility for search queries, and has a simple in-memory cache with TTL. Show me how to use it in a React component.
Frequently asked questions
What is the default email rate limit in Supabase?
The default SMTP allows 2 auth emails per hour. This includes signup confirmations, password resets, magic links, and email change confirmations. Configure a custom SMTP provider to remove this limit.
What are the API request limits per plan?
Free plan has approximately 500K API requests per month. Pro plan ($25/month) has unlimited API requests. These limits apply to REST API calls through PostgREST, not direct database connections.
How quickly do rate limits reset?
Email rate limits reset on a rolling 1-hour window. API rate limits vary — most reset within 60 seconds. Check the Retry-After header in the 429 response for the exact wait time.
Can I increase rate limits without upgrading my plan?
For email limits, yes — configure custom SMTP. For API limits on the free plan, you need to either optimize your code to make fewer requests or upgrade to Pro. On Pro plans, contact Supabase support for custom rate limit adjustments.
Does Supabase rate limit database connections?
Yes. The free plan allows up to 50 direct connections and 200 pooled connections via Supavisor. Pro plan allows more. Use connection pooling (port 6543) for server-side applications to stay within limits.
How do I know if my app is close to hitting rate limits?
Check Dashboard > Settings > Usage for current API request counts and database metrics. Set up monitoring with log drains (Pro plan) to get alerted before hitting limits.
Can RapidDev help optimize my app to avoid rate limits?
Yes. RapidDev can audit your Supabase usage, implement caching strategies, configure SMTP, optimize queries, and set up monitoring to keep your application within rate limits.
Talk to an Expert
Our team has built 600+ apps. Get personalized help with your project.
Book a free consultation