> For the complete documentation index, see [llms.txt](https://kb.craftformswp.com/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://kb.craftformswp.com/documentation/for-developers/submission-limits-and-spam-protection.md).

# Submission Limits and Spam Protection

CraftForms protects your forms from floods of fake entries, tampered prices and dangerous files. Most of this works without any setup. This article lists every limit a visitor (or you, while testing) can run into, what the visitor sees, and how a developer can change it.

None of these limits has a setting in the admin. A developer changes them with a filter or a constant, usually in a small custom plugin or the theme's `functions.php`.

### Quick reference <a href="#quick-reference" id="quick-reference"></a>

| What                                                | Default                                                                     | Message the visitor sees                                           |
| --------------------------------------------------- | --------------------------------------------------------------------------- | ------------------------------------------------------------------ |
| Submissions from one visitor to one form            | 10 per minute                                                               | *Too many submissions. Please wait a moment and try again.*        |
| Submissions from one visitor to all forms           | 30 per 5 minutes                                                            | same                                                               |
| Submissions with the same email address to one form | 5 per hour                                                                  | same                                                               |
| Security token on a cached page                     | valid 12–24 hours                                                           | *Invalid security token.*                                          |
| Price sent by the browser                           | must match the server's price                                               | *Price verification failed. Please reload the page and try again.* |
| Uploaded file size                                  | the field's **Max size** (5 MB if empty), capped by your server's PHP limit | shown under the field                                              |
| Signature                                           | 600 KB                                                                      | the field counts as empty                                          |

### Rate limits <a href="#rate-limits" id="rate-limits"></a>

CraftForms counts submissions per visitor (by IP address) and per email address. When a count is reached, the next submission is refused with **Too many submissions. Please wait a moment and try again.** (HTTP 429) until the time window has passed.

| Limit             | Default                       | Counted by                                       |
| ----------------- | ----------------------------- | ------------------------------------------------ |
| Per form          | 10 submissions per 60 seconds | form + visitor's IP                              |
| Per site          | 30 submissions per 5 minutes  | visitor's IP, across all forms                   |
| Per email address | 5 submissions per hour        | form + the first email address in the submission |

**Who isn't limited:** logged-in users who can access CraftForms (administrators, editors, shop managers). This is why you may never see the message yourself. **Test rate limits in a private window, logged out.**

**Testing tip:** a few test runs in a row from the same computer can use up the 30-per-5-minutes allowance. Wait five minutes, or use a different email address for each test (the per-email limit is separate).

#### Change the limits <a href="#change-the-limits" id="change-the-limits"></a>

```php
add_filter( 'craftforms_submit_rate_limits', function ( $limits, $form ) {
    $limits['per_form']['max']  = 20;   // 20 per minute per form
    $limits['per_email']['max'] = 0;    // 0 turns this limit off
    return $limits;
}, 10, 2 );
```

Each entry is `[ 'max' => number, 'window' => seconds ]`. Setting `max` or `window` to 0 turns that limit off. `$form` is the form's post, so you can raise the limits for one busy form only.

#### Sites behind Cloudflare or another proxy <a href="#sites-behind-cloudflare-or-another-proxy" id="sites-behind-cloudflare-or-another-proxy"></a>

If your site sits behind Cloudflare, a load balancer or a similar proxy, visitors can appear to come from the proxy's IP address. Visitors then share one allowance and see *Too many submissions* at random.

**Check first.** Open a few recent entries under **Submissions** and look at the **IP** shown for each. If they show your visitors' different addresses, everything already works and you don't need to change anything. Many hosts already pass the real address to WordPress. If most entries show the same few addresses, and they belong to your proxy, read on.

By default CraftForms uses only the address of the computer that connected to your server. Headers such as `X-Forwarded-For` are ignored, because a visitor can type any value into them. If CraftForms trusted them, a bot could send a new fake address with every request and never reach a limit.

**Cloudflare.** Add this to a small custom plugin or your theme's `functions.php`:

```php
add_filter( 'craftforms_trusted_proxy_headers', function () {
    return [ 'HTTP_CF_CONNECTING_IP' ];
} );
```

Cloudflare always writes this header itself and replaces any value a visitor sends.

**Other proxies.** Ask your host which header the proxy uses for the visitor's address and whether it **replaces** a value sent by the visitor, or only adds to it. Trust only a header that it replaces, using the same filter. For example, nginx with `proxy_set_header X-Real-IP $remote_addr;` gives `HTTP_X_REAL_IP`.

**Quick option.** Adding this line to `wp-config.php` trusts `CF-Connecting-IP`, `X-Forwarded-For` and `X-Real-IP` together:

```php
define( 'CRAFTFORMS_TRUST_PROXY_HEADERS', true );
```

CraftForms reads `X-Forwarded-For` from the right, so a made-up address a visitor puts at the start is ignored. With more than one proxy in a row, though, the address found may be one of your proxies, and visitors would share one allowance again. Naming the one header your proxy sets is the safer choice.

> **Important:** only trust a proxy header when your server accepts traffic **from the proxy alone**. If anyone can reach the server directly, for example by its IP address, they can send the header themselves and avoid the rate limits. With Cloudflare, ask your host to allow web traffic from Cloudflare's IP addresses only, or use Cloudflare Tunnel or Authenticated Origin Pulls.

#### Other rate limits <a href="#other-rate-limits" id="other-rate-limits"></a>

| Feature                                               | Default                        | Change with                                                                     |
| ----------------------------------------------------- | ------------------------------ | ------------------------------------------------------------------------------- |
| Abandoned form capture (saving drafts)                | 20 saves per minute per IP     | not changeable                                                                  |
| Public Catalog data (booking calendars, availability) | 120 requests per minute per IP | `craftforms_catalog_public_read_rate_limit` filter, same `max` / `window` shape |

### Spam protection <a href="#spam-protection" id="spam-protection"></a>

* **Behaviour check in the browser.** While a visitor fills in a form, CraftForms looks at how it was filled in, such as typing, focus and how quickly the form was sent. A submission that looks automated is stopped with *Spam submission detected.* Developers can listen for the `spam-detected` event on the form element, for example to log it in analytics.
* **Rate limits** (above) stop one visitor or one email address from sending a flood of entries.
* **Security token.** Every form carries a token that the site checks on submit (see below).
* **Only known fields are kept.** Anything sent that isn't a field on the form is thrown away, and every field is validated again on the server: required, email, URL, length, number range and pattern rules.

**Add your own check.** The `craftforms_pre_submit` filter runs before anything is saved or sent. Return a `WP_Error` to refuse the submission. The error's `status` sets the HTTP code.

```php
add_filter( 'craftforms_pre_submit', function ( $result, $data, $form, $form_meta, $request ) {
    if ( my_spam_service_says_spam( $data ) ) {
        return new WP_Error( 'spam', __( 'Your message could not be sent.', 'my-site' ), [ 'status' => 403 ] );
    }
    return $result;
}, 10, 5 );
```

### Security token and page caching <a href="#security-token-and-page-caching" id="security-token-and-page-caching"></a>

Each form carries a security token that WordPress creates when the page is generated. It stays valid for **12 to 24 hours**. If a caching plugin or your host serves an older copy of the page, every submission fails with **Invalid security token.**

Fix it with any of these:

* exclude pages with forms from the page cache;
* set the cache to expire at least every 12 hours;
* purge the cache after the message appears.

Forms shown on other websites (embeds and headless sites) need **Allow External Submissions** turned on in the form's submission settings (PRO). Without it they're refused with *External submissions are not enabled for this form.*

### Price checks (PRO) <a href="#price-checks-pro" id="price-checks-pro"></a>

The price the visitor sees is calculated in the browser, but it is never trusted. The server works out the price again from the submitted fields, the form's smart variables (including User role variables) and the price formula.

| Situation                                                  | What happens                                                                   |
| ---------------------------------------------------------- | ------------------------------------------------------------------------------ |
| The browser's price differs from the server's              | Refused: *Price verification failed. Please reload the page and try again.*    |
| The server can't calculate a price at all                  | Refused, never charged at the browser's price                                  |
| Booking capacity changed while the visitor was on the page | Refused: *Booking capacity has changed. Please reload the page and try again.* |

Smart variables shown in submissions and emails (**Expose** turned on) are saved with the server's value too.

The usual cause of *Price verification failed* is an outdated page: the visitor had the page open while you changed prices, or a cached copy was served. Reloading the page fixes it.

### File uploads (PRO) <a href="#file-uploads-pro" id="file-uploads-pro"></a>

| Limit                                                   | Default                                                                                                          | Change with                                   |
| ------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------- | --------------------------------------------- |
| Size of each file                                       | the field's **Max size**, or 5 MB, but never more than your server's PHP `upload_max_filesize` / `post_max_size` | `craftforms_file_upload_max_bytes` (in bytes) |
| Number of files                                         | the field's **Max files**                                                                                        | `craftforms_file_upload_max_files`            |
| File types                                              | only the types ticked on the field. If none are ticked, every file is refused                                    | `craftforms_file_upload_allowed_types`        |
| Images from Image Cropper, Layered Image, Picture Frame | 8 MB each                                                                                                        | `craftforms_interactive_image_max_bytes`      |
| Signature image                                         | 600 KB                                                                                                           | not changeable                                |
| Images placed inside a PDF                              | 15 MB each; larger images are left out of the PDF                                                                | not changeable                                |

CraftForms checks each file's real type from its contents, not just its name, and renames the extension to match. Some types are **always refused**, whatever the field allows: scripts and programs (PHP, Python, shell, `.exe`, `.bat`, JavaScript and others), web pages (`.html`, `.htm`), **SVG** and XML files, and server files such as `.htaccess`.

Uploaded files wait in a temporary folder until the form is submitted. Unsubmitted files are deleted after about an hour. Files that were stored but never linked to a submission are removed after 2 days (`craftforms_unowned_file_grace_days`).

### Abandoned form capture <a href="#abandoned-form-capture" id="abandoned-form-capture"></a>

When [abandoned form capture](#abandoned-form-capture) is on, each saved draft keeps at most 200 fields and 5,000 characters per field. Password fields, files, signatures and payment details are never saved. The same visitor's draft is updated rather than duplicated for 30 minutes (`craftforms_abandoned_recapture_delay`, in seconds). Drafts are deleted after 30 days (`craftforms_abandoned_retention_days`).

### Emails and connected services after a submit <a href="#emails-and-connected-services-after-a-submit" id="emails-and-connected-services-after-a-submit"></a>

* The first email of a submission is sent straight away. Further emails, integrations and webhooks run **after** the visitor sees the success message, 2 seconds apart, so mail services with a per-second limit aren't overloaded. Change the gap with `craftforms_deferred_email_stagger_seconds`, or per action type with `craftforms_deferred_action_stagger_seconds`.
* On servers without PHP-FPM, these run before the success message instead, and without the gap.
* Waiting times for outside services: webhooks 15 seconds, email-marketing integrations 10 seconds, payment providers 15–20 seconds.
* Integrations that fail because the service is down are retried after 1, 5 and 30 minutes (PRO). Webhooks are not retried.

### How long logs are kept <a href="#how-long-logs-are-kept" id="how-long-logs-are-kept"></a>

| Log                  | Kept for              | Change with                                 |
| -------------------- | --------------------- | ------------------------------------------- |
| Integration Activity | 30 days               | `craftforms_integration_log_retention_days` |
| Flow Runs (PRO)      | 30 days               | `craftforms_flow_run_retention_days`        |
| Abandoned entries    | 30 days               | `craftforms_abandoned_retention_days`       |
| Email Logs           | until you delete them | not changeable                              |

<br>
