Rate limits
Every form has a per-IP rate limit. The same IP submitting the same form too fast gets a 429 Too Many Requests response. Real users almost never hit it; bots hammering one form do.
Limits are enforced before validation, before captcha, before anything expensive. A blocked request is cheap.
Default limits
Two limits apply to every form, independent of plan:
| Scope | Submissions | Window |
|---|---|---|
| Per IP, per form | 20 | 60 seconds |
| Per team (all forms combined) | 120 | 60 seconds |
The per-IP limit is the polite default for a single submitter. The per-team ceiling is the safety net against a single leaked endpoint being abused from many IPs at once.
These defaults are sane for a typical contact or signup form. If you run a campaign that legitimately bursts above 20 submissions per minute from the same IP (rare), captcha tuning is usually the better lever than raising the rate limit - see Captcha.
What a 429 looks like
HTTP/1.1 429 Too Many Requests
Retry-After: 42
Content-Type: application/json
{
"error": "rate_limited",
"message": "Too many submissions from this IP. Try again in 42 seconds.",
"retry_after": 42
}
The Retry-After header is in seconds and matches the retry_after field in the JSON body. Clients should respect it - retrying earlier just resets the cooldown.
For browser-side submissions, the response includes CORS headers so your fetch handler can read the body. Display a friendly "you're going too fast" message rather than a generic error.
What doesn't count
- Authenticated REST API calls (those use a separate per-token limit).
- Webhook deliveries from Formspring to your endpoint (those are throttled by the queue).
- Step-validation requests (e.g.
validate-steppreviews) - only completed submissions count.
If you're consistently hitting the per-team ceiling on legitimate traffic, the bottleneck is volume across all your forms combined - increase per-form captcha strength or contact support before tuning anything else.
Handling 429 in your code
If you submit from JavaScript, parse the Retry-After header and back off:
async function submitForm(formId, data) {
const res = await fetch(`https://formspring.io/f/${formId}`, {
method: 'POST',
body: data
});
if (res.status === 429) {
const retryAfter = Number(res.headers.get('Retry-After') ?? 30);
throw new RateLimitError(`Try again in ${retryAfter}s`, retryAfter);
}
return res.json();
}
For server-side submissions, treat 429 as a hard failure - don't loop. If you're hitting the limit from a server (where retries are cheap), you're hitting the wrong limit; raise the per-form cap or use the authenticated REST API instead.