# A Form Stopped Submitting After You Enabled Caching, and the Nonce Is Usually Innocent

> A WordPress nonce expires on a shared clock at midnight and noon UTC, not on your page's schedule. Three causes break a cached form, and the famous one is rarest.

- Published: 2026-10-08
- Updated: 2026-10-08
- Author: xSpeed Cache Team
- Tags: Caching, Troubleshooting, WooCommerce
- Canonical: https://xspeedcache.com/blog/form-stopped-working-after-caching/

---

## Quick Summary: Three Causes, and the Famous One Is Rarest

A WordPress nonce is valid for between 12 and 24 hours, and Contact Form 7 6.2, released 6 October 2026 to more than 10 million installs, ships with nonce verification switched off. Both facts come from the shipped source, read today. The explanation everyone reaches for first is, on most sites, the wrong one.

| What you see | What it means | The fix |
|:---|:---|:---|
| Console error, no network request | The handler never bound | Exclude one script from delayed JavaScript |
| Request returns HTML, not JSON | The endpoint was cached | Exclude the AJAX or REST path |
| Success message, no email | A dead nonce, logged as spam | Cache lifetime under 12 hours |

Jump to: [the one-load check](#confirm-it-before-you-change-a-setting) · [the nonce clock](#a-nonce-dies-on-a-shared-clock) · [exclusions](#what-to-exclude-and-in-what-order)

## Confirm It Before You Change a Setting

One page load separates the three. Open the form in a private window with the console and network panel open, submit, and read the result against the table above. The third row wastes days, because every surface reports success.

## A Nonce Dies on a Shared Clock

WordPress core computes the window from the wall clock, in [`wp_nonce_tick()`](https://developer.wordpress.org/reference/functions/wp_nonce_tick/):

```php
$nonce_life = apply_filters( 'nonce_life', DAY_IN_SECONDS, $action );
return ceil( time() / ( $nonce_life / 2 ) );
```

Read that against the [`nonce_life` filter](https://developer.wordpress.org/reference/hooks/nonce_life/) default of 86,400 seconds and the arithmetic falls out. The tick is a 12-hour block counted from the Unix epoch, so every boundary lands at exactly 00:00 and 12:00 UTC. [`wp_verify_nonce()`](https://developer.wordpress.org/reference/functions/wp_verify_nonce/) accepts the current tick and the one before it, which its own source in `wp-includes/pluggable.php` states as "valid for between 12 and 24 hours".

![A one-day UTC timeline marked at 00:00 and 12:00. A page cached at 11:59 keeps its nonce 12 hours; one cached at 12:01 keeps nearly 24.](https://xspeedcache.com/images/blog/d19-nonce-clock.webp)

A nonce's remaining life therefore depends on when in the half-day the page was stored, not on its age.

| Page cached at | Nonce valid until | Useful life |
|:---|:---|:---|
| 00:01 UTC | 00:00 UTC next day | 23h 59m |
| 11:59 UTC | 00:00 UTC next day | 12h 01m |
| 12:01 UTC | 12:00 UTC next day | 23h 59m |

Because the boundary is shared, every cached page expires together, twice a day, which is why this arrives as a cliff rather than a drift. A lifetime at or above 12 hours therefore guarantees some visitors are served a dead nonce, and setting it below 12 removes the cause more cheaply than any exclusion. Our [cache exclusions guide](https://xspeedcache.com/blog/wordpress-cache-exclusions-setup/) covers the alternative.

## Contact Form 7 Does Not Nonce a Logged-Out Visitor

Here the famous diagnosis collapses. Contact Form 7's [`wp-contact-form-7.php`](https://plugins.svn.wordpress.org/contact-form-7/trunk/wp-contact-form-7.php) defines its constant as `false`:

```php
if ( ! defined( 'WPCF7_VERIFY_NONCE' ) ) {
	define( 'WPCF7_VERIFY_NONCE', false );
}
```

Per the same release, [`includes/submission.php`](https://plugins.svn.wordpress.org/contact-form-7/trunk/includes/submission.php) returns early for anyone not signed in:

```php
if ( ! $this->contact_form->nonce_is_active() or ! is_user_logged_in() ) {
	return true;
}
```

So on a default install a stale nonce cannot be the cause: none is printed or checked for the logged-out visitor filling in the form. Where verification is active and the nonce is dead, that same file raises no error. It sets `$spam = true` with the reason "Submitted nonce is invalid", so the form thanks the visitor and the mail is dropped.

| Form | Nonces a logged-out visitor? | Failure visible? |
|:---|:---:|:---:|
| Contact Form 7 6.2, default | ❌ | — |
| Contact Form 7, verification on | ❌ | ❌ logged as spam |
| WooCommerce add-to-cart | ✅ | ✅ |

WooCommerce is where the clock really bites. Its [`includes/class-wc-cache-helper.php`](https://plugins.svn.wordpress.org/woocommerce/trunk/includes/class-wc-cache-helper.php) sends no-cache headers through `prevent_caching()`, but the shipped condition limits that to cart, checkout and account pages. Shop and product pages get none, so an add-to-cart form on a cached product page is exposed. Our [WooCommerce checkout guide](https://xspeedcache.com/blog/how-to-optimize-your-woocommerce-checkout-page/) and the [use-case page](https://xspeedcache.com/use-cases/woocommerce/) cover what that store needs.

## When the Handler Never Bound

If the console named a file and no request left the browser, the cache stored the page correctly and an optimizer changed the script order: a delayed script runs in a document that already fired the event it was waiting for, so the listener never binds. We worked that through in [Delay JavaScript Broke the Menu](https://xspeedcache.com/blog/delay-javascript-broke-menu/), and the fix is the same, which is to exclude the one file the console named rather than the whole feature.

![Three lanes showing where a submission is lost: the page cache fails on the clock, the optimizer stops the handler binding, the endpoint cache stores the response.](https://xspeedcache.com/images/blog/d19-three-lanes.webp)

## When the Endpoint Itself Got Cached

This cause has grown more common, for a reason worth stating. `admin-ajax.php` sits under `/wp-admin/`, which page caches skip by default, so forms built on it were accidentally safe. The faster endpoints are not. WooCommerce's `WC_AJAX::get_endpoint()` builds a front-end URL of the form `/?wc-ajax=<action>`, and the REST API answers at `/wp-json/`. Both are paths a page cache will store.

A cached endpoint returns a stale JSON body, or your homepage's cached HTML where the script expected JSON. Exclude the path rather than the page: the [REST API caching section of our docs](https://xspeedcache.com/docs/feeds-and-api/#rest-api-caching) records which routes are safe to store. If a logged-out visitor is also seeing someone else's content, that is a different fault and [we covered it separately](https://xspeedcache.com/blog/cached-pages-showing-logged-in-content/).

## What to Exclude, and in What Order

Work down this list, stopping at the first change that fixes it.

1. 🕐 **Drop the page-cache lifetime below 12 hours.** This removes the nonce cause entirely.
2. 🎯 **Exclude the single script the console named** from delayed JavaScript or combining.
3. 🛣️ **Exclude the AJAX or REST path**, not the calling page.
4. 🧾 **Exclude the form's own page**, only if the three above failed.

Turning optimization off site-wide is absent on purpose: it changes four things at once, so it tells you nothing about which mattered. That is why our own plugin keeps each behind its own switch, with the exclusion fields in the [free tier](https://xspeedcache.com/free-vs-pro/) rather than behind the [paid plans](https://xspeedcache.com/pricing/). xSpeed Cache is ours, built at WPDeveloper.

## Common Mistakes People Make Chasing a Stale Nonce

- 🧯 **Blaming the nonce before reading the console.** On a default Contact Form 7 install there is no nonce to go stale, and a [builder form fails for its own reasons](https://xspeedcache.com/blog/best-caching-plugin-elementor-page-builders/).
- ⏱️ **Raising the nonce lifetime with `nonce_life`.** It works, and it widens the window in which a stolen credential is still good.

## Frequently Asked Questions

### My form says it was sent successfully but I never get the email. Where did it go?

Check the spam log before the cache settings. Where verification is on and the nonce has expired, Contact Form 7 marks the submission as spam rather than failing it, so every surface reports success.

### My contact form worked all morning and broke after lunch. Why would caching do that on a schedule?

Because the schedule is real. Nonce windows are counted from the Unix epoch in 12-hour blocks, so they roll over at 00:00 and 12:00 UTC for every site at once.

### I turned off my caching plugin and the form started working. Does that prove the plugin is at fault?

It narrows it to one of three layers, which is not the same as naming one. Switching the plugin off disables page caching, script delay and combining at once, so re-enable it and turn off one feature at a time.

### Do I need to exclude my whole contact page from caching?

Usually not, and it is the last resort rather than the first. A lifetime under 12 hours, or an exclusion on the endpoint path, fixes the common cases while the page stays cached.

### My AJAX product filter returns the homepage instead of results. Is this the same bug?

That is the endpoint cause. The filter calls a front-end path such as `/?wc-ajax=` or a `/wp-json/` route, and the cache stored a response for that URL.

## Conclusion: Read the Console, Then the Clock

A form that stopped submitting after you enabled caching is rarely a mystery and rarely the nonce. One load in a private window separates the three causes, and each fix is a narrow exclusion rather than a switched-off feature. Where the nonce is the culprit, the cure is arithmetic: keep the lifetime under 12 hours. Our [speed scan](https://xspeedcache.com/scan/) reports what your site caches and for how long, free and with no account, and [common problems and troubleshooting](https://xspeedcache.com/docs/common-problems-and-troubleshooting/) is the docs page to keep open.
