Supabase says “email rate limit exceeded”: what it means and how to fix it
What the 429 means, the built-in sender’s cap and team-only rule, the Resend SMTP settings, and the limits and CAPTCHA to keep afterwards.
By Sameem Amjad
The short answer
Supabase Auth returns “email rate limit exceeded” (HTTP 429, error code over_email_send_rate_limit) when your project has asked its email sender for more confirmation, magic-link or password-reset emails in the last hour than the sender allows. On the built-in sender that cap is 2 emails per hour for the whole project, as of 30 October 2026. Since 26 September 2024 that sender also delivers only to members of your Supabase organisation. The fix is custom SMTP: connect a provider such as Resend on the dashboard's SMTP settings page, then raise the email limit on the Rate Limits page to match your real sign-up rate.
Test it in 10 seconds
In the Supabase dashboard open Authentication, then the SMTP settings page; its URL ends in /auth/smtp. Custom SMTP switched off is red: every auth email still goes through the built-in sender, with its 2-an-hour cap and its team-only delivery. The Management API gives the same answer from the terminal. Create a personal access token at supabase.com/dashboard/account/tokens and run:
export SUPABASE_ACCESS_TOKEN="YOUR_ACCESS_TOKEN"
export PROJECT_REF="YOUR_PROJECT_REF"
curl -s "https://api.supabase.com/v1/projects/$PROJECT_REF/config/auth" \
-H "Authorization: Bearer $SUPABASE_ACCESS_TOKEN" \
| jq '{external_email_enabled, smtp_host, rate_limit_email_sent}'external_email_enabled: false is the same red. true, with your provider's host in smtp_host, is green, and rate_limit_email_sent is then the hourly limit your project enforces in place of the built-in cap.
Why it happens
Supabase gives every project a built-in email sender so you can try Auth and design your templates. Its own docs call that sender best-effort, not for production, with limits that “can change without notice”. Two of those limits produce this error in a live app, and a third gets mistaken for it.
The first is the hourly cap. The built-in sender sends 2 emails per hour across the whole project while Confirm email is on, and Confirm email is on by default for hosted projects. Supabase has moved that number more than once: it cut the cap to 4 per hour in July 2023, and its rate-limits page gives 2 on 30 October 2026. Every confirmation, magic link, invite, reset and email-change notice counts, so a sign-up form with Confirm email on fails for the third new user of the hour. Testing your own form burns the two before a customer arrives.
The second is who the sender delivers to. Since 26 September 2024, a project without custom SMTP delivers auth emails only to members of the Supabase organisation that owns it. Everyone else gets “Email address not authorized” (code email_address_not_authorized). Your real users were never going to receive mail from the built-in sender, whatever the cap said.
The third is the per-address cooldown. Supabase makes the same address wait 60 seconds between confirmation, magic-link or reset emails. That check returns the same 429 and the same over_email_send_rate_limit code, but its message reads “For security purposes, you can only request this after N seconds.” It is per user, it is there on purpose, and custom SMTP does not remove it.
A plain request without an API version header, which is what curl sends, gets the error in this shape:
{
"code": 429,
"error_code": "over_email_send_rate_limit",
"msg": "email rate limit exceeded"
}A client that sends X-Supabase-Api-Version: 2024-01-01 gets code as the error string and the text under message instead. supabase-js wraps either as an AuthApiError with status 429 and code set to the string, so branch on error.code and never on the message text.
The fix: custom SMTP through Resend
Supabase Auth works with any service that speaks SMTP; its docs list Resend, AWS SES, Postmark, Twilio SendGrid, ZeptoMail and Brevo. The steps below use Resend because its Supabase settings are four fields and it is the first name on that list. Postmark works the same way with its own host and credentials.
In Resend, add your domain and verify it. Resend sends only from a verified domain and recommends a subdomain over the root, so verify something like auth.yourdomain.com and keep the root for people. Verification is three DNS records that Resend lists for you: an MX record and a TXT record for SPF, both on the host Resend names, and a TXT record at resend._domainkey for DKIM. Resend says this can take up to 72 hours and is often much faster. Then create an API key in Resend.
In the Supabase dashboard open Authentication, go to the SMTP settings page (the URL ends in /auth/smtp), switch custom SMTP on and enter:
Sender email: no-reply@auth.yourdomain.com
Sender name: Your App
Host: smtp.resend.com
Port: 465
Username: resend
Password: YOUR_RESEND_API_KEYThe sender address must sit on the domain you verified. Save, and from that moment Supabase sends auth emails to any address through Resend, with an hourly limit of 30 messages. Supabase sets that figure low on purpose to protect the reputation of a sending domain that has no history yet. The same seven fields can be set through the Management API (external_email_enabled, smtp_host, smtp_port, smtp_user, smtp_pass, smtp_admin_email, smtp_sender_name) if you manage projects from scripts.
Raise the hourly limit, keep the cooldown
30 an hour is more than the built-in sender gave you and less than a launch needs. If custom SMTP was already on when the error appeared, you are above that figure or inside the 60-second cooldown, and this page is where to look. Open Authentication, then Rate Limits (the URL ends in /auth/rate-limits) and set the email limit from your busiest hour of sign-ups and resets, with room for a launch day. Do not set it to a number you will never reach. A limit far above your traffic lets a bot turn your password-reset form into a machine that sends thousands of emails from your domain. The bounces then wreck a reputation your domain has not had time to earn.
Leave the 60-second per-address window alone; a real user does not need two reset emails in one minute. Keep Confirm email on as well. Switching it off makes this error disappear because Supabase stops sending confirmations, and it also lets anyone sign up with an address they do not own and have Supabase mark it confirmed. The toggle lives under Authentication, Sign In / Providers, and it is the first thing to check if a previous developer “fixed” this error in an afternoon.
Supabase's Send Email hook is the alternative: it hands each message to a function you write instead of an SMTP server. Use it if your provider has no SMTP endpoint, or you want React email templates, a queue or a fallback provider. For an app that needs its sign-up emails working this week, SMTP is the shorter road.
Two side effects to handle
Bots come first. Supabase's SMTP guide describes the pattern. Someone signs up lists of known addresses with preset passwords, in a trickle over months or in one burst. The aim is to burn your sending reputation, to block real sign-ups and resets, or to pressure you into switching email confirmation off. A working sender and a higher limit make your forms worth attacking. Supabase's answer, and mine, is CAPTCHA on the sign-up, sign-in and reset forms. Supabase supports hCaptcha and Cloudflare Turnstile. Switch it on under Authentication, Bot and Abuse Protection (the URL ends in /auth/protection), paste the provider's secret key, then pass the widget's token with each call:
await supabase.auth.signUp({
email,
password,
options: { captchaToken },
})Deliverability comes second. Resend's verification gave you SPF and DKIM; add a DMARC record for the sending domain as well, which Supabase lists alongside the other two. Keep auth mail off the domain and the From address you use for marketing, and keep promotional lines out of the templates. Leave the templates alone once they work: Supabase's guide warns that marketing-shaped auth mail is what spam filters catch. The links in auth emails point at your project's Auth server. Supabase says a custom domain for the project reduces the chance of spam filters punishing your messages for another project's bad reputation.
One more thing bites during development: every test sign-up on the hosted project spends real quota. The Supabase CLI runs Auth on your machine and routes every email to Mailpit, with no hourly limit and no team restriction:
supabase start
supabase statussupabase status prints the local API URL (http://127.0.0.1:54321 by default) and the Mailpit URL (http://127.0.0.1:54324). Point your app's Supabase URL and publishable key at the local values and read the emails in Mailpit.
Which layer rejected the mail
Two failures look alike from the browser. The 429 is Supabase refusing to send. A 500 with the message “Error sending confirmation email” (or “Error sending recovery email”, or “Error sending magic link email”) and the code unexpected_failure is your SMTP provider refusing the mail after Supabase handed it over. The usual reasons are a wrong API key, a sender address on a domain Resend has not verified, or a port your host blocks. Supabase keeps the provider's reason out of the response and writes it to the request's log entry instead.
Open Logs in the dashboard (the URL ends in /logs) and set Log Type to Auth. Filter Status to 429 or 500, or type over_email_send_rate_limit into the search box; plain text matches anywhere in the event message. Each entry carries the status, the request path and the message under metadata, and the 500 entries carry the provider's error text as well, which the browser never sees. A 429 on the sign-up or recover path with the rate-limit code is the cap described above. A 500 whose error names the SMTP server is the provider, and the fix is in Resend's dashboard, not in Supabase.
Test it before a customer does
Sign up with an address outside your Supabase organisation; a personal Gmail address will do. Before the fix the request fails with email_address_not_authorized. After it the mail arrives from your sender name and address, and the raw headers (Gmail's “Show original”) show SPF and DKIM passing for the domain you verified. Then request a password reset for the same address twice inside a minute: the second attempt should fail with the “For security purposes” message, which proves the cooldown survived your rate-limit changes. Run the 10-second test again and read true for external_email_enabled. For the first week after launch, look at the Auth logs for 429s. A steady run of them on the recover path with no matching real users is a bot at your reset form, and CAPTCHA is the answer.
When the email limit isn't the only problem
An app that reached paying users on the built-in sender has often left other defaults in place too. The usual ones are Confirm email switched off during testing, the Site URL still on localhost so reset links land nowhere, and tables without row-level security (RLS). The guide on Supabase's “RLS disabled in public” warning covers the tables. If your app was built with Lovable, Bolt, Replit or Cursor and has paying users waiting on these emails, that is what my launch-readiness audit covers. You get a fixed list of what will fail in production, in the order it will fail, and the work to close it.