What the 429 means
429 is the HTTP status for rate limiting. RFC 6585 defines it as a client that “has sent too many requests in a given amount of time”, and says the answer may carry a Retry-After header with the wait. It deliberately leaves out how the server counts: per IP address, per form, per account or per anything else is the service's choice.
So a 429 tells you two things. Your form code and the endpoint are fine, because the server answered. And this particular post was not taken. What it does not tell you is which limit you hit or for how long, unless the service says so in the body or in Retry-After.
What 30 quick posts got from 10 form backends
| Service | First 429 | Retry-After | What the 429 said | CORS header on the 429 | Documented limit |
|---|---|---|---|---|---|
| Formgong | None in 30 | Not reached | Not reached | Yes, on every answer | 20 per 10 s per IP, then 20 a minute per IP and 30 a minute per form |
| Formspree | Post 21 | None | {"error":"Too many requests"} | Yes, your site's origin | 20 posts a minute |
| Splitforms | Post 7 | None | "code":"rate_limited", “Wait a minute and try again.” | Yes | 6 a minute and 20 per 15 minutes, per form and IP |
| Static Forms | Post 10, from its hosting firewall | None | {"error":{"code":"429","message":"Too Many Requests",…}} | None | Not readable: its docs blocked our reader |
| Forminit | None in 30 | Not reached | Not reached | Not reached | 1 per 5 s per IP |
| Formcarry | None in 30 | Not reached | Not reached | Not reached | 1 per 15 s per form and IP |
| Basin | None in 30 | Not reached | Not reached | Not reached | Not stated in the docs we read |
| Formspark | None in 30 | Not reached | Not reached | Not reached | Not stated in the docs we read |
| Web3Forms | None: all 30 got 403 (treated as a server) | Not reached | Not reached | Not reached | 429 documented, “wait for one hour” |
| FormSubmit | No answer: 521 outage on its side | Not stated in the docs we read |
Formspree and Splitforms stopped us exactly where their docs say they would. Static Forms' 429 came from Vercel, the platform it is hosted on, with the header x-vercel-mitigated: deny, so it was the hosting firewall talking and not the form service. None of the three 429 answers had a Retry-After header or any RateLimit header, so code that waits for one will not find it.
Read the “None in 30” rows carefully. We used made-up keys, so nothing was stored and nobody got an email, and a service may count only posts to a form that exists. A 429 here means the limit runs before the key is checked. No 429 says nothing about what a real form on that service does: Forminit and Formcarry document limits that our wrong-key posts never reached. FormSubmit answered every request with error 521, an outage on its side, so its row has no result. Formgong's own row shows the same thing from the inside: the per-minute limits run after the key is looked up, so a wrong key never meets them, and 30 posts in 25 seconds stayed under the 20-per-10-second brake. The contact form API page has the other answers from the same test.
Why a form backend sends 429, most likely first
- Too many posts from one IP address. This is the limit most services have, and the one you hit while testing: you press Send, change a field, press Send again. Everyone behind one office router or school network shares an address too. So does every visitor when your own server forwards the form, which is why Formcarry's docs tell you to post from the browser.
- Too many posts to one form. Some limits count all visitors together. A newsletter that sends a crowd to the contact page in the same minute, or a bot hammering the form, can use it up, and then real visitors get the 429 too.
- A firewall in front of the service. Cloudflare, Vercel and other hosts block traffic they think is a flood before the form service sees it. The body then looks unlike the service's normal errors, and it may lack CORS headers, which matters in the browser (below).
- Your page sends more than you think. A double-click on a button with no lock, Enter and a click together, or a script that retries every failure at once. Each of these doubles the count, and a retry loop on 429 keeps the limit hit for as long as it runs.
- The monthly allowance is used up. Some services answer 429 for that as well: Splitforms' docs list 429 for both cases, so read the code in the body. Waiting a minute does not help with this one; the plan or the month has to change.
To see which one you have, read the body before anything else. A service's own JSON with a code such as rate_limited is its limiter. A page from Cloudflare or Vercel is the firewall. Then count the POST requests in the browser's Network tab for one click.
Why load tests and double posts set it off
A load test fired at a form endpoint from one laptop is, to the service, one IP address posting dozens of times a minute. That is exactly what a per-IP limit exists to stop, so most of your test measures the 429 path. Spread the test across many machines and you move on to the per-form limit, which also turns away whoever was really trying to reach you during those minutes.
A load test with a made-up key, like ours, does not show where the real limit sits either, as the table above shows. If you need to know how your form behaves under a crowd, test your own page and send two or three real posts to the service, or ask the service for its numbers. It is their server, and their docs usually state the limit. Hammering a vendor's endpoint mostly tells their firewall about you.
Double posts are the everyday version of the same problem. Each extra copy counts toward the limit, so a form that sends twice per click hits a 20-a-minute limit after 10 messages. Lock the button while the request runs; our guide to duplicate form submissions counted which locks actually stop the second post.
How it shows up in fetch and in a plain form post
With fetch, a 429 does not throw. The promise resolves, res.ok is false and res.status is 429, so code that only catches errors treats it as success or as a generic failure. Check the status. The exception is a 429 without CORS headers, such as the hosting firewall's answer in our table: the browser then hides the response from your page, and you get TypeError: Failed to fetch instead of a status. Our Failed to fetch guide explains how to tell that apart from a real network error in the console.
With a plain HTML form, there is no code of yours in between. The browser goes to whatever the service sends back, which can be a raw JSON line, a text page or the service's own error page, and the visitor's message is not sent. Formgong answers a plain form's 429 with a redirect to a page that says “Too many submissions in a short time. Try again shortly.” in the visitor's language. You can change that text in the form's settings, and the per-minute limit can send visitors to your own error page instead.
Handle it in code
Three rules cover a contact form. Show the visitor a wait instead of an error. Use Retry-After when it is there and a sensible default when it is not. And do not resend the message on their behalf: the visitor clicked once, so the page should post once, and the button comes back when the wait is over. The handler below does that, and it also locks the button so a double-click cannot double the count.
const MAX_WAIT = 60; // seconds; never ask a visitor to wait longer than this
// Retry-After is either a number of seconds or an HTTP date (RFC 9110).
function retryAfterSeconds(res) {
const value = res.headers.get("Retry-After");
if (!value) return null;
if (/^\d+$/.test(value.trim())) return Number(value);
const date = Date.parse(value);
return Number.isNaN(date) ? null : Math.max(0, Math.ceil((date - Date.now()) / 1000));
}
const form = document.querySelector("#contact");
const button = form.querySelector("button[type=submit]");
const status = form.querySelector("[role=status]");
form.addEventListener("submit", async (event) => {
event.preventDefault();
if (button.disabled) return; // a second click or Enter while sending
button.disabled = true;
status.textContent = "Sending…";
let res;
try {
res = await fetch("https://formgong.com/submit", {
method: "POST",
headers: { Accept: "application/json" },
body: new FormData(form),
});
} catch {
// No answer you can read: offline, blocked, or a firewall's 429 without CORS headers.
status.textContent = "Could not reach the server. Check your connection and press Send again.";
button.disabled = false;
return;
}
if (res.status === 429) {
const wait = Math.min(retryAfterSeconds(res) ?? MAX_WAIT, MAX_WAIT);
status.textContent = "Too many messages right now. You can send again in " + wait + " seconds.";
// No automatic resend: the button comes back and the visitor decides.
setTimeout(() => { button.disabled = false; status.textContent = ""; }, wait * 1000);
return;
}
const data = await res.json().catch(() => null);
if (res.ok && data?.success !== false) {
form.reset();
status.textContent = "Thanks, your message was sent."; // the button stays disabled
return;
}
status.textContent = data?.message || "Something went wrong. Please try again.";
button.disabled = false;
});Backoff for scripts that post many submissions
// For a script or a queue that posts many submissions (an import, a backfill), not for a visitor's click.
async function postWithBackoff(url, payload, tries = 5) {
for (let attempt = 0; attempt < tries; attempt++) {
const res = await fetch(url, {
method: "POST",
headers: { "Content-Type": "application/json", Accept: "application/json" },
body: JSON.stringify(payload),
});
if (res.status !== 429) return res; // success or a real error: do not repeat it
const header = res.headers.get("Retry-After");
const seconds = header && /^\d+$/.test(header) ? Number(header) : 2 ** attempt * 5; // 5, 10, 20, 40 s
// A little random jitter so parallel workers do not all come back in the same second.
await new Promise((resolve) => setTimeout(resolve, (seconds + Math.random() * 2) * 1000));
}
throw new Error("Still rate limited after " + tries + " tries");
}When retrying is safe
Repeating a post after a 429 is safe in principle, because the server said it did not take it. Formgong's /submit sends its rate_limited and limit_exceeded answers before the submission is saved, so a later retry cannot create a copy. Repeating after a timeout or a dropped connection is different: the first post may have arrived, and the retry becomes a duplicate.
Backoff belongs in scripts and queues, where nobody is waiting at a button: an import of old submissions, or a server that forwards leads. Wait the Retry-After seconds, or longer each time if there is none, with a little random jitter so parallel workers do not come back together, and give up after a few tries. Retry only on 429, never on every error. A wrong key answers 404 and will answer 404 on the fifth try as well.
The same applies the other way round. If your webhook receiver answers a form backend with 429, the backend counts it as a failed delivery and tries again later on its own schedule. Formgong's retries follow fixed waits and ignore Retry-After, as described in what happens when a form webhook fails.
How Formgong's limits work
- A burst brake first. More than 20 posts in 10 seconds from one IP address get a 429 with
Retry-After: 10, before the post is even read. - Then two per-minute limits: 20 posts a minute from one IP address, and 30 a minute to one form from everyone together. They count by the clock minute, so a refused post can go through again at the start of the next minute. This 429 carries
Retry-Afterwith the seconds left until the next minute, from 1 to 60, so your code knows exactly how long to wait. Posts already refused for one address do not use up the form's budget. - A readable answer. JSON posts get
{"success":false,"code":"rate_limited","message":"…"}, with CORS headers on every answer, so your fetch code always sees the status. - Spam does not count toward the monthly limit. It goes to the Spam folder and the sender sees the normal success answer. Only past 100 spam posts to one form in a day does the rest get a 429.
- The monthly limit rarely means 429. Over the limit, new submissions are still accepted and kept. A 429 with the code
limit_exceededcomes only when the store of held submissions is full as well.
The access key in your form is public by design, and these limits are part of why that is fine. The access key article covers what else stops someone who copies it.
Still getting 429?
Which service sent it, and what did the body say? Write to support@formgong.com with the status, the body and the headers from the Network tab. If it is a case we have not covered, we will look into it and add it here. And if something on this page sent you the wrong way, we want to hear that too.
What Formgong's free plan includes
- Build the form or bring your own. The form builder takes the colours and fonts from your site's address, with no sign-up; ready form templates copy into your account; AI writes the fields from one sentence. Any hand-written HTML form works too.
- Websites with the form already connected. 28 free website templates post to Formgong out of the box.
- 250 submissions a month on unlimited forms. Spam does not count. Once a year, a busy month keeps delivering on credit until the 7th instead of stopping.
- Everywhere you work. Email to up to 3 people at once, Telegram with Reply and Handled buttons, Google Sheets, Notion, and 3 signed webhooks for Slack, Discord, Zapier, Make or n8n.
- Emails to your customers in your business's name. Formgong sends them under your business name and replies come back to you, with no email service of your own to set up: a review request to every lead, a reminder a visitor asked for, and review emails after an order with your logo and colours. People who agree can also go straight into your Mailchimp, Klaviyo, Brevo, MailerLite or Kit list. An automatic reply to each visitor is on Pro.
- No captcha needed. A server-side spam filter, a honeypot, allowed domains and a block list; add your own Turnstile key if you want one.
- Files and voice. Visitors can attach files, or record a voice message that arrives transcribed.
- Made for AI-built sites. A hosted form page and a contact button; step-by-step guides for sites built with Lovable, Bolt, v0, Replit and Base44; an MCP server lets Cursor or Claude wire the form for you.
- See what the form does. Analytics for views, starts, conversion and where each lead came from, and an alert when a form goes quiet.
- One account for the rest. Customer reviews and an AI help chat that answers visitors from your own pages, with every message in one inbox. Data is stored in the EU.
Frequently asked questions
What does 429 Too Many Requests mean on a contact form?
The form backend refused this post because too many arrived from your IP address, or to your form, in a short time. The form itself works. Wait, then send once.
How long should I wait after a 429?
As many seconds as the Retry-After header says. If there is none, wait about a minute: most form limits count per minute. In our test on 29 October 2026, none of the three services that answered 429 sent Retry-After.
Why do I get 429 when I test my form but my visitors do not?
Testing sends many posts from one IP address in a few minutes, which is what per-IP limits catch. Your visitors each post once from their own address.
Should my form retry automatically after a 429?
Not for a visitor's click. Show the wait and let them press Send again. Automatic backoff with Retry-After belongs in scripts and queues, and only for 429, not for every error.
Does spam count toward Formgong's limits?
Not toward the monthly limit: spam goes to the Spam folder and is not counted. It does count toward the per-minute limits, and past 100 spam posts to one form in a day the rest get a 429.
Sources and documentation
Official references for this guide: RFC 6585: 429 Too Many Requests, RFC 9110: Retry-After, MDN: 429 Too Many Requests, Formspree: system limits, Splitforms docs, Forminit llms.txt, Formcarry: what every form needs, Web3Forms: API reference, Our raw test results. HTML contact form, where data is stored.
Related guides
- Lovable form submissions to email and Telegram
- HTML contact form without a backend: a working example
- How to send website form submissions to Telegram
- Contact form not sending email? Check where it stops
- Managing website leads in Telegram without a CRM
- Form backend for Lovable, Bolt, v0, Cursor
- Telegram bot for a contact form: build or skip?
- GDPR form backend: 7 checks before you choose
- Lovable form not sending email? 6 fixes
- Netlify Forms not working? React and Bolt fixes
- Stop contact form spam without a CAPTCHA
- GitHub Pages contact form: a working setup
- Mailto Form in HTML: Why It Fails and What to Use
- HTML form to Google Sheets: 2 free methods
- Webflow form submission limit: what to do at 50
- Turnstile vs reCAPTCHA vs hCaptcha for forms
- Contact Form 7 and Elementor forms to Telegram
- Squarespace contact form not sending email?
- Shopify contact form: where do messages go?
- Google Form to Telegram: free Apps Script way
- Wix contact form not sending email? Fixes
- EU / GDPR Formspree alternatives compared
- HTML form action attribute explained
- How do HTML forms work? The HTTP request
- Types of Injection Attacks on Web Forms (2026)
- Indirect Prompt Injection in MCP
- MCP Rug Pull Attack: Detect Tool Changes
- Form without a backend: 7 ways that work
- Thank-you page after form submission (HTML)
- Indirect Prompt Injection Examples (2023–2026)
- Indirect Prompt Injection via Email
- What Is Tool Poisoning in MCP?
- WordPress contact form without a plugin
- Send email from frontend JavaScript
- How to Prevent Indirect Prompt Injection
- Honeypot Form Field: How to Add One That Works
- Contact Form with File Upload (HTML, No PHP)
- Angular contact form without a backend
- Send form submissions to Slack or Discord without Zapier
- Verify a form webhook signature (HMAC-SHA256)
- Contact forms that send nothing: 793 AI-built sites tested
- v0 contact form that actually sends: 3 ways
- How we tested AI-built contact forms, and 12 bugs we hit
- Cloudflare vs Netlify free plan: hosting that never pauses
- EmailJS 412 “Gmail_API: Invalid grant”: reconnect Gmail
- Resend “You can only send testing emails”: verify a domain
- Supabase Edge Function blocked by CORS policy: 3 causes
- Formspree “Form not found” and other errors: fixes
- Web3Forms errors: “Invalid access key” and 403 explained
- How to add customer reviews to your website for free
- Getform not working? “This form has been deleted” and more
- Next.js contact form not working? 6 errors, reproduced
- Webflow form not sending email? What to check, tested
- FormSubmit not working? “This form needs Activation” fix
- Contact form “TypeError: Failed to fetch”: causes and fixes
- WooCommerce product enquiry form
- Contact Form 7 vs WPForms
- Gravity Forms alternative free
- WordPress contact form block
- 535-5.7.8 Username and Password not accepted (Gmail)
- 535 5.7.3 Authentication unsuccessful: Office 365 SMTP
- Nodemailer “Missing credentials for "PLAIN"”: the fix
- Nodemailer “Greeting never received”: port 465 fix
- Nodemailer SSL “wrong version number”: the port fix
- Nodemailer “self-signed certificate in certificate chain”
- Nodemailer ECONNREFUSED 127.0.0.1:587: missing SMTP host
- SendGrid “authorization grant is invalid”: 401 fix
- Mailgun 401 Forbidden: why the API rejects your key
- “Edge Function returned a non-2xx status code”: the fix
- Next.js “Body exceeded 1 MB limit”: Server Action fix
- Unexpected token '<', "<!DOCTYPE "... is not valid JSON
- “TypeError: Body is unusable”: body has already been read
- Turnstile “timeout-or-duplicate” and other siteverify errors
- Turnstile vs hCaptcha vs reCAPTCHA: speed and size tested
- Judge.me reviews not showing on Shopify: how to fix it
- Judge.me review carousel not showing: how to fix it
- Yotpo reviews not showing on Shopify? Check the app embed
- Loox reviews not showing on product page: how to fix
- Shopify reviews not showing: what to check first
- How to show reviews on Shopify
- GDPR for website forms: what each type of form needs
- WooCommerce reviews not showing: how to fix it
- Stamped.io reviews not showing on Shopify: how to fix it
- Etsy reviews not showing: why and what to do
- Trustpilot widget not loading: how to fix TrustBox
- Yotpo problems in 2026: what merchants report
- Contact Form 7 “error trying to send your message” fix
- Elementor form “Your submission failed because of an error”
- Framer form not sending email or not working: what to check
- Carrd contact form not working: plans, spam and errors
- Google review link not working: what customers see and why
- tawk.to widget not showing on your website: causes and fixes
- How to add questions and answers to your product page
- “Failed to send a request to the Edge Function”: fixes
- Next.js “Failed to find Server Action”: what it means
- Brevo “unauthorized: Key not found” 401: the fix
- Amazon SES “Email address is not verified”: the fix
- Shopify contact form not working: how to fix it
- Judge.me alternative for WooCommerce after the 2025 shutdown
- Free WooCommerce review plugins compared on the free version
- WooCommerce review reminder emails without a paid plugin
- Add Voice Recording to a Website Form
- Contact Form 7 spam: how to stop it
- WPForms not sending email: how to fix it
- Contact form emails going to spam: how to fix it
- HTML form not submitting: how to fix it
- Contact form not working: how to find the fault
- Webflow form spam: how to stop it
- Website form to Notion: how to set it up
- Website form to WhatsApp: what works and what does not
- What 1-star reviews say about AI support chatbots
- Which free form backend gives you the most submissions?
- Elfsight reviews widget not showing? Check the view limit
- Next.js contact form backend vs Server Actions
- Review stars not showing in Google search results
- How to stop duplicate form submissions
- How to see which form leads came from ChatGPT
- What happens when a form webhook fails
- When to send a review request email after an order
- How to collect video testimonials for free
- Add contact form sign-ups to Mailchimp or Klaviyo
- How to migrate from Formspree without losing submissions
- How to get a Google review link for your business
- What is a headless form backend?
- Is it safe to put a form access key in your HTML?
- AI chatbot that answers only from your website
- Minified React error #441 in Next.js
- Squarespace product reviews after Judge.me
- Accessible contact form checklist for WCAG 2.2
- How to ask a client for a testimonial by email
- How to respond to negative reviews, with examples
- MCP server for forms in Cursor and Claude
- Can you offer a discount for a review?
- Do EU stores have to say if reviews are verified?
- Webflow form backend options compared
- How to add review schema JSON-LD to a product page
- How to turn a customer review into an Instagram post
- How to make a QR code for Google reviews
- Live chat or chatbot for a small business website
- Why people abandon your contact form halfway
- Which form backend do AI assistants pick? 54 answers
- PHP mail() not sending email, even when it returns true
- What “Failed to fetch” means, tested in three browsers
- 405 Method Not Allowed when you submit a form
- Send a contact form to more than one email address
- What is review syndication, and who needs it?
- How to save HTML form data to a database
- NetworkError when attempting to fetch resource
- Send product reviews to Google Merchant Center
- PHPMailer not working on a live server
- AI review reply generator: does it help?
- React 19 contact form with useActionState
- Astro Actions or a form backend?
- Is no-reply@wixsiteautomations.com legit?
- Where to find the Judge.me review widget code
- AI review summaries: what shoppers actually see
- Photo reviews for WooCommerce, and the GPS inside them
- Review request email templates you can copy
- Reviews in AI search: what ChatGPT and Perplexity cite
- Lead capture form with UTM tracking, built and tested
- Form design best practices, with our own form data
- Client intake form template with file upload
- What is social proof? Meaning and examples
- What is user-generated content (UGC)?
- Benefits of negative reviews for your store
- How to get your first reviews as a new store
- How to use customer reviews in your marketing
- How to get more customer reviews
- React form validation without a library, tested
- jQuery submit form with validation
- reCAPTCHA v3 setup with the server score check
- Shopify social proof that shoppers can check
- HTML file input: accept, multiple, capture and size checks
- How to add hCaptcha to a form, with the server check
- How to add invisible reCAPTCHA to a form
- Bug report form template with browser details
- What the FTC fake reviews rule bans for online stores
- How to show reviews on your homepage
- Do reviews increase sales? What the studies show
- HTML input types and what each one sends
- HTML textarea, tested in a real form
- HTML dropdown menu in a form, tested
- Do reviews help SEO? What Google says and shows
- How to add a review carousel people can read
- Product review examples that help shoppers decide
- Which trust badges actually mean something
- Customer satisfaction survey with CSAT and NPS
- JavaScript FormData and what fetch actually sends
- JavaScript form validation without a library
- Server-side form validation and what it catches
- Email validation in JavaScript, tested on 33 addresses
- Zod form validation with FormData, tested
- Store reviews vs product reviews
- How to send review requests through Klaviyo
- How to tag customer reviews by topic and mood
- Custom review form questions worth asking
- Online reputation management for small businesses
- HTML radio button groups and what they send
- HTML label tag and what screen readers hear
- HTML autocomplete attribute, tested with Chrome autofill
- HTML fieldset and legend, and what disabled leaves out
- HTML button tag and what a form button sends
- Vite contact form that still works after the build
- How to test a contact form, end to end
- How to embed a Google Form, and what it loads
- Customer feedback questions that get real answers
- How to convert a file to base64 in JavaScript
- FilePond with a form backend, and when to skip it
- Bootstrap form validation and server errors
- Tailwind form validation, tested in three browsers
- Donation form in HTML, and where the payment goes
- How to ask for a review in person or by text
- Which review sites matter for your business
- What are customer reviews, and which kinds count
- Online review statistics, checked at the source
- Best review app for Shopify and WooCommerce
- Why Gmail sends your self-hosted mail to spam
- htmx contact form that sends to another domain
- AI form builders tested, from prompt to sent form
- Headless WordPress forms with Contact Form 7, tested
- React Hook Form watch vs useWatch, and what gets sent
- Elfsight widget not working? It vanishes when views run out
- Jotform not sending emails? Check Jotform's bounce list
- Typeform not working? A white box means a blocked script
- Positive review response examples that don't sound copied
- How to remove a Google review when you can't delete it
- Google reviews not showing up? 8 causes to check in order
- Is buying Google reviews illegal? Even a review swap counts
- Which reviews can you hide? A review moderation checklist
- Average star rating: what 4.8 from 5 reviews means
- Reviews on a product page: where and how many to show
- SaaS testimonials: 3 of 27 had a number in them
- Do reviews reduce returns? Not when they're too good
- Testimonial examples for business: what makes one believable
- Testimonial questions to ask clients, by type of business
- Testimonial page design: a page or a section?
- Testimonial advertising rules: what the FTC requires
- Form submission canceled because the form is not connected
- Input type date format dd/mm/yyyy: what you can change
- How to disable textarea resize, or allow only vertical
- How to prevent Enter from submitting a form
- form.submit() vs requestSubmit(): what submit() skips
- Custom HTML checkbox CSS that doesn't break required
- How to reset a form after submit, and what reset() keeps
- Input type number maxlength not working? What to use
- How to create an HTML form, step by step
- Best form builder: free plans and branding, checked
- Best WordPress contact form plugin: 10 free ones compared
- How to increase form conversion rate: start with the count
- Form backend vs form builder: which one do you need?
- WordPress contact form not sending email? 3 plugins tested
- Contact Form 7 webhook? Turning it on switched our email off
- Gravity Forms webhook: what it sends and the URLs it rejects