Form returns 429: burst limit or exhausted quota?

Form returns 429: burst limit or exhausted quota?

8 min read
Static Forms Team
Contact formsTroubleshootingRate limitsJavaScript
Share:

A contact form returning HTTP 429 has reached a limit, but the status alone does not tell you which one. Sending the same request again might work after a short burst subsides. It will not fix an exhausted monthly allowance, an unpaid overage invoice, or a temporarily blocked account.

This runbook helps you identify the limiting layer before you retry. It includes an offline JavaScript classifier and tests for representative responses. The classifier sends nothing, needs no API key, and deliberately leaves unfamiliar errors unresolved.

Capture the failing request before changing anything

Open your browser's Network panel and select the failed request. Record its URL path, HTTP status, response content type, error message, and timestamp with timezone. Keep the request body private. A screenshot or HAR export can contain contact details, cookies, and tokens; redact it before sharing.

First check which request failed. A 429 from a CAPTCHA challenge endpoint is not a 429 from https://api.staticforms.dev/submit. A proxy or hosting firewall can also reject a request before it reaches the form backend. An HTML challenge page is a reason to investigate that layer, not proof that your monthly submission quota is empty.

The Static Forms API reference lists 429 for monthly limits, submission rate limits, and ALTCHA rate limits. JSON errors use an error message; native form responses can be plain text. Read the actual response rather than assuming every failed request contains JSON.

If you do not have a captured request, make one controlled test after reviewing your usage and spam configuration. Do not run a loop to reproduce the limit. A real test can consume quota and trigger notifications or integrations.

Separate the short window from the billing window

Use the message together with the workspace dashboard. These are distinct checks, even when they share HTTP 429.

The message names submissions per minute or per hour

Treat this as a short-window submission limit. Look for accidental request amplification: duplicate event listeners, repeated clicks, a polling script, or a bot burst. Inspect Network for several POSTs caused by one interaction. The duplicate-submission guide covers the browser-side pending guard.

Do not create extra keys or spread requests across accounts to evade the control. Stop the source of the burst and wait for the relevant window. A per-hour error is not a promise that waiting one minute will fix it. If the response says the account is temporarily blocked, follow that message and contact support when necessary.

The message names the monthly submission limit

Open Workspace → Plan & usage. Confirm the workspace that owns the form, the usage count, its allowance, and the displayed period. Do not assume the counter resets on the first day of the calendar month. The plan documentation describes a rolling usage window; use the range shown for your workspace rather than calculating a reset from the word “monthly.”

Free and Starter stop at their allowance. Pro and Agency support opt-in overage, but being on an eligible plan does not mean it is enabled. Check the current pricing and overage terms before making a billing change. If the error says overage billing is paused because of an unpaid invoice, resolve that billing issue instead of adding a timer to your form.

Retrying the same request cannot increase the allowance. Wait for the applicable reset, change the plan, or configure eligible overage deliberately. Recheck the dashboard after the change; do not assume a checkout screen proves that the form is ready.

The message names email deliveries or an account block

An hourly email-delivery limit is different from an hourly submission limit. Preserve the exact wording. A blocked delivery can have a stored submission record, so a 429 is not a universal guarantee that nothing was stored.

Search the correct Inbox and inspect any matching submission before sending again. If a request was accepted but its notification is missing, continue with the email-delivery runbook. Repeated frontend POSTs are not an email-delivery repair.

Classify captured responses without sending another request

Save this as diagnose-429.mjs. It accepts a status and a captured response body. It does not fetch a URL, read credentials, log the body, or schedule a retry. The returned kind values belong to this example; they are not Static Forms API error codes.

JavaScript
export function diagnose429(status, body) {
  if (status !== 429) {
    return { kind: 'not-429', automaticRetry: false };
  }

  let value = body;
  if (typeof value === 'string') {
    try {
      value = JSON.parse(value);
    } catch {
      // Native form errors can be plain text.
    }
  }

  const message = typeof value === 'string'
    ? value.trim()
    : value && typeof value === 'object'
      && typeof value.error === 'string'
      ? value.error.trim()
      : '';

  let kind = 'unknown';
  if (/^Account temporarily blocked due to sustained rate limit violations\./i.test(message)) {
    kind = 'account-block';
  } else if (/^Monthly submission limit \(\d+\) exceeded\./i.test(message)
    && /overage billing is paused due to an unpaid invoice/i.test(message)) {
    kind = 'billing-action';
  } else if (/^Monthly submission limit \(\d+\) exceeded for your account tier /i.test(message)) {
    kind = 'monthly-quota';
  } else if (/^Rate limit exceeded: maximum \d+ submissions per minute\./i.test(message)) {
    kind = 'submission-minute';
  } else if (/^Rate limit exceeded: maximum \d+ submissions per hour for this API key\./i.test(message)) {
    kind = 'submission-hour';
  } else if (/^Rate limit exceeded: maximum \d+ email deliveries per hour for this API key\./i.test(message)) {
    kind = 'delivery-hour';
  }

  return { kind, automaticRetry: false };
}

There are no placeholders to replace. Use the captured numeric HTTP status and response body as inputs if you integrate this function into an internal diagnostic tool. Keep that tool separate from the public form's submit handler.

These matches are hints based on current message wording, checked on 23 October 2026. They are not a versioned machine-readable contract. If the service changes its wording, the safe result is unknown, not a guessed waiting period. The helper also cannot authenticate the origin of a pasted message. Confirm the request URL and dashboard evidence yourself.

Save the following alongside it as diagnose-429.test.mjs. Run node --test diagnose-429.test.mjs with a current Node.js release. Every response below is a synthetic fixture, not a request to production.

JavaScript
import test from 'node:test';
import assert from 'node:assert/strict';
import { diagnose429 } from './diagnose-429.mjs';

const cases = [
  ['submission-minute', 429, {
    success: false,
    error: 'Rate limit exceeded: maximum 10 submissions per minute. Please wait a moment before submitting again.'
  }],
  ['submission-hour', 429, {
    error: 'Rate limit exceeded: maximum 30 submissions per hour for this API key. Please try again later.'
  }],
  ['monthly-quota', 429, {
    error: 'Monthly submission limit (250) exceeded for your account tier (free). Please upgrade your plan to accept more submissions.'
  }],
  ['billing-action', 429, {
    error: 'Monthly submission limit (25000) exceeded. Overage billing is paused due to an unpaid invoice. Please update your payment method.'
  }],
  ['delivery-hour', 429, {
    error: 'Rate limit exceeded: maximum 30 email deliveries per hour for this API key. Please try again later.'
  }],
  ['account-block', 429,
    'Account temporarily blocked due to sustained rate limit violations. Please try again later or contact support.'],
  ['monthly-quota', 429, JSON.stringify({
    error: 'Monthly submission limit (250) exceeded for your account tier (free). Please upgrade your plan to accept more submissions.'
  })],
  ['unknown', 429, '<html><body>Request blocked</body></html>'],
  ['unknown', 429, '{broken JSON'],
  ['unknown', 429, null],
  ['unknown', 429, { error: 123 }],
  ['unknown', 429, { error: 'A new provider message' }],
  ['not-429', 200, { success: true }],
  ['not-429', 403, { error: 'CAPTCHA verification failed.' }],
  ['not-429', 500, { error: 'Server error' }]
];

for (const [index, [kind, status, body]] of cases.entries()) {
  test(`captured response ${index}: ${kind}`, () => {
    const result = diagnose429(status, body);
    assert.equal(result.kind, kind);
    assert.equal(result.automaticRetry, false);
  });
}

The numeric limits in these fixtures only exercise parsing. They are not a plan table or a capacity recommendation. Use your workspace's current configuration for operational decisions.

Decide whether another submission is safe

RFC 6585, section 4 says a 429 response may include Retry-After; it does not require that header. Do not invent a countdown when the response supplies none. A waiting hint also does not override a monthly quota or billing error.

Use this retry checklist:

  • Known short-window rejection: remove the source of repeated requests, wait for the relevant window, then consider one deliberate test. Check for an existing record first if the response concerns delivery or the earlier outcome is unclear.
  • Monthly quota or billing action: resolve the allowance or billing condition before another test. Automatic backoff will keep hitting the same constraint.
  • Unknown 429 or HTML response: identify which layer answered and inspect its logs or support guidance. Do not treat an unfamiliar body as a monthly-limit diagnosis.
  • Timeout or network failure without a readable response: the outcome is unknown. Check the Inbox before resubmitting. Closing a browser connection does not prove that processing stopped.

Keep visitors' text in the current form when a request fails. Give them an understandable message and an alternative contact route if you have one. Do not expose invoice details to visitors, write their message to a public URL, or persist it in browser storage just to support retries. Preserve your existing labels, keyboard operation, visible focus, and live status announcements when changing error handling.

Verify the fix on the deployed form

Test the exact deployed page, not just the local JavaScript function. Confirm that one click or keyboard submission produces one POST, an HTTP failure never displays “sent,” and a rejected request does not erase the visitor's message. Keep CAPTCHA and domain restrictions enabled as configured; disabling spam protection is not a rate-limit fix.

After the cause is resolved, send one authorized, clearly identified test. Record the response, check for the matching Inbox entry, and verify the destination separately. Stop once you have that evidence. The offline tests above prove classification and the absence of an automatic-retry decision; they do not prove your quota reset, production acceptance, or mailbox delivery.