In WordPress, the nonce lifetime is one day for every action unless you change it. That default causes problems in two directions. A destructive action like “delete my account” keeps a valid token for up to 24 hours. A newsletter form on a page-cached front end can show a nonce that has already expired, so visitors get nonce verification failed or “The link you followed has expired” errors.
The snippet below sets a separate lifetime for each nonce action. Short-lived tokens protect sensitive actions, longer ones serve cached forms, and every other nonce in core and your plugins keeps the default.
The snippet
Put it in a small plugin, an mu-plugin or your theme’s functions.php. Change the action names in the array to match the ones you pass to wp_create_nonce(), wp_nonce_field() or wp_nonce_url().
<?php
/**
* Per-action nonce lifetimes, in seconds.
* Keys are the exact action strings passed to wp_create_nonce() / wp_nonce_field().
*/
function dpc_nonce_lifetimes(): array {
$lifetimes = array(
'dpc_delete_account' => 10 * MINUTE_IN_SECONDS,
'dpc_newsletter_signup' => 2 * DAY_IN_SECONDS,
);
return (array) apply_filters( 'dpc_nonce_lifetimes', $lifetimes );
}
add_filter( 'nonce_life', 'dpc_nonce_life', 10, 2 );
function dpc_nonce_life( $lifetime, $action = -1 ) {
if ( ! is_string( $action ) || '' === $action ) {
return $lifetime;
}
$map = dpc_nonce_lifetimes();
if ( ! isset( $map[ $action ] ) ) {
return $lifetime;
}
// Never go below two minutes: a nonce is only guaranteed valid for half its lifetime.
return max( 2 * MINUTE_IN_SECONDS, (int) $map[ $action ] );
}
Verification code doesn’t change. Your handler checks the nonce the usual way, together with a capability check:
$nonce = isset( $_POST['_wpnonce'] ) ? sanitize_text_field( wp_unslash( $_POST['_wpnonce'] ) ) : '';
if ( ! wp_verify_nonce( $nonce, 'dpc_delete_account' ) || ! current_user_can( 'read' ) ) {
wp_die( esc_html__( 'This link has expired. Please reload the page and try again.', 'dpc' ), 403 );
}
How it works
Since WordPress 6.1, the nonce_life filter receives the nonce action as its second argument. Both wp_create_nonce() and wp_verify_nonce() call wp_nonce_tick( $action ), and that function reads the filter. Because the lifetime is looked up by action, creation and verification always use the same value. Any action that isn’t in the array returns the default untouched.
A WordPress nonce doesn’t expire at an exact moment. Time is split into “ticks” of half the lifetime, and a nonce is accepted for the current tick and the previous one. A 10-minute nonce therefore lasts between 5 and 10 minutes. wp_verify_nonce() returns 1 if the nonce was created in the current tick and 2 if it was created in the previous one. That’s also why the snippet sets a minimum: a 60-second lifetime could expire after 30 seconds, before the user finishes the form.
Customise it
- Add actions from another plugin or theme without editing the snippet:
add_filter( 'dpc_nonce_lifetimes', fn( $map ) => $map + array( 'my_export' => HOUR_IN_SECONDS ) ); - For cached front-end forms, set the lifetime longer than your page cache TTL, or the cached HTML will keep serving expired tokens.
- If a single admin screen uses several actions, give each one its own entry. The keys must match the action strings exactly.
Gotchas
- Leave
wp_restandheartbeat-noncealone. The block editor and REST requests depend on them, and a shortwp_restlifetime logs editors out of API calls mid-edit. To lock the REST API down, use WordPress REST API Restricted to Authenticated Users instead. - Nonces without an action (the default
-1) are skipped on purpose. Always pass a named action, both for this snippet and for security. - A nonce is not a capability check. It proves intent, not permission, so always pair it with
current_user_can(). - For logged-out visitors, a nonce is tied to the action and user ID 0, so every guest gets the same token for the same tick. Treat it as CSRF protection, not as authentication.
- Changing a lifetime invalidates nonces already printed on open pages, because the tick size changes. Expect a few failed submissions right after you deploy.
Nonces are one layer. Keeping core patched matters just as much: WordPress Security Updates Email Alert Without a Plugin emails you when a security release is waiting.
Snippet poll. Should this snippet be in the Scripts Organizer library for one-click import? 10 votes put it there.
0 of 10 votes