You click Upload, Save or Submit, and WordPress replies with a bare page: “The link you followed has expired. Please try again.” The page title may say “An error occurred.” Older WordPress versions, and some plugins, show “Are you sure you want to do this?” instead. In the block editor or behind a form, the same failure can appear as “Cookie check failed”, a response of -1, or a silent 403.
All of these mean one thing: WordPress expected a valid security token, called a nonce, with your request and did not get one. Either the token had expired, belonged to a different login session, or never arrived at all. The last case is the surprise: the most common trigger is uploading a theme or plugin ZIP that is larger than your PHP limits, which empties the whole request. This guide is for site owners and designers who hit the message in wp-admin or on a form.
What is happening
A nonce is a short hash WordPress puts in forms and admin links to prove the request came from a screen it generated for you. In the core source (wp-includes/pluggable.php) it is built from four pieces: a time counter, the action name (such as theme-upload), your user ID and your login session token, all hashed with the NONCE_KEY and NONCE_SALT secrets from wp-config.php.
The time counter comes from wp_nonce_tick(). It reads the nonce_life filter, which defaults to one day (86,400 seconds), and divides time into ticks of half that length, 12 hours. wp_verify_nonce() accepts a nonce from the current tick (returning 1) or the previous tick (returning 2). So a nonce is valid for between 12 and 24 hours, depending on when in the check it was made.
| What invalidates the nonce |
Typical situation |
| No nonce in the request |
Upload bigger than post_max_size, so PHP discards all form fields |
| Time |
A tab left open overnight, or a cached page served for more than 12 to 24 hours |
| Session token changed |
You logged out in another tab, your session expired, or a security plugin ended it |
| Different user |
A cached copy of a page made for another user, or for logged-out visitors |
| Salts changed |
New keys in wp-config.php invalidate every existing nonce and login |
| Action mismatch |
A plugin bug creates the nonce under one name and checks another |
When an admin screen check fails, check_admin_referer() calls wp_nonce_ays(), which prints the “link you followed has expired” page. AJAX checks with check_ajax_referer() answer -1 with status 403 instead. The REST API answers rest_cookie_invalid_nonce with the message “Cookie check failed”.
Before you start
You need an administrator login. For limit changes you need your hosting panel PHP settings, or SSH and the ability to edit PHP configuration files. Back up any file before you edit it.
How to run commands: SSH into the server and change into the site folder, the one containing wp-config.php. WP-CLI may be wp or wp-cli on your host. curl runs on your own computer; it is built into macOS, Linux and Windows 10 and later (or use WSL). The diagnostic tools can fetch headers from a browser.
Step 1: Uploads larger than your PHP limits
This is the classic cause when the error appears right after you upload a theme or plugin ZIP in Appearance, Themes, Add New or Plugins, Add New.
Why it happens
PHP has two limits that matter: upload_max_filesize, the largest single file, and post_max_size, the largest whole request. When a request exceeds post_max_size, PHP throws away the entire body. $_POST and $_FILES arrive empty, including the nonce field. WordPress looks for the nonce, finds nothing, and reports that the link expired. Nothing actually expired; the request was emptied before WordPress saw it.
Confirm it
Go to Media, Add New and read “Maximum upload file size”, or Tools, Site Health, Info, Server, which lists “Upload max filesize” and “PHP post max size”. If your ZIP is bigger than either, you have found the cause.
The PHP error log usually confirms it with a line like this:
PHP Warning: PHP Request Startup: POST Content-Length of 73400320 bytes exceeds the limit of 67108864 bytes in Unknown on line 0
This is example log output, not a command. The first number is the size of your request in bytes and the second is post_max_size in bytes (67108864 is 64 MB). The exact wording varies slightly between PHP versions. If the line appears at the time of your upload, the limit is your cause.
If the server returns “413 Request Entity Too Large” instead, the web server refused the upload before PHP saw it; on Nginx that is client_max_body_size. Upload limits across PHP, Nginx, Apache and proxies are covered in depth in the upload limits guide.
Fix it
Raise both limits in your hosting panel PHP settings, keeping post_max_size equal to or larger than upload_max_filesize. If you edit the configuration yourself, the directives look like this in php.ini or a .user.ini file in the site folder:
upload_max_filesize = SIZE_LIMIT
post_max_size = POST_LIMIT
SIZE_LIMIT is the largest file you need, such as 64M. POST_LIMIT should be the same or a little larger, such as 72M, because the request also carries form fields. Changes to .user.ini can take a few minutes to apply on PHP-FPM; your panel may also need a PHP restart. Good result: Site Health shows the new values. Bad result: the old values persist, which means another configuration file wins; ask your host.
Or skip the browser upload
Upload the ZIP over SFTP to the site folder, then install it on the server:
wp theme install THEME_FILE.zip --force
wp plugin install PLUGIN_FILE.zip --force --activate
THEME_FILE.zip and PLUGIN_FILE.zip are the paths to your uploaded files. --force overwrites an existing copy of the same theme or plugin, which you want when updating; leave it off for a first install. --activate activates the plugin after installing. Good output ends with Success: Installed 1 of 1 themes. (or plugins). WP-CLI uses the command-line PHP configuration, which often has higher limits than the web server, so this route usually works even before you fix the limits. Delete the ZIP afterward.
Step 2: Stale tabs and ended sessions
If the error appears when you save a settings screen or approve something you opened a long time ago, the nonce simply aged out, or your session ended.
Confirm it
Ask: was the tab open for many hours, or did you log out, change your password or log in from another browser since? Logging out destroys your session token, and every nonce built from it becomes invalid immediately, even ones made a minute ago.
Fix it
Copy any text you typed, reload the screen to get fresh nonces, and submit again. If the problem keeps happening within minutes, a plugin or the server is ending your session: see Step 4.
The block editor and Heartbeat
The editor stays open for hours, so WordPress refreshes its nonces in the background through the Heartbeat API. Core hooks wp_refresh_post_nonces and related functions to the wp_refresh_nonces filter, which runs on Heartbeat requests to admin-ajax.php. Speed and security plugins often offer to disable Heartbeat. If it is disabled on the post editor, long editing sessions eventually fail to save with “Cookie check failed” or an “Updating failed” notice. Keep Heartbeat on for the editor; slow it down instead of switching it off. Slow wp-admin and Heartbeat tuning are covered in the slow dashboard guide.
Step 3: Cached admin screens and forms
If the error appears for several people, or right after a cache or CDN change, a cache may be serving copies of pages that contain someone else’s nonce or an old one.
Confirm it
Run this on your own computer:
curl -sI https://example.com/wp-login.php | grep -iE "cache|age"
curl -sI https://example.com/wp-admin/ | grep -iE "cache|age"
example.com is your domain. -s hides progress, -I fetches headers only, and grep -iE filters cache-related headers case-insensitively. Good output shows no cache hit: cf-cache-status: DYNAMIC or BYPASS, and cache-control containing no-cache or no-store. Bad output is cf-cache-status: HIT, x-cache: HIT, x-litespeed-cache: hit or an age header on these URLs: something is caching pages that must never be cached.
Fix it
- In Cloudflare, check Cache Rules for a “cache everything” rule. Add a bypass for paths starting with
/wp-admin/, for /wp-login.php, and for requests with a cookie starting wordpress_logged_in_.
- In your cache plugin, make sure caching for logged-in users is off, or uses a separate cache per user.
- Purge every cache layer, then test in a private window.
Front-end forms that fail because their page was cached for more than a day are covered step by step in the guide on forms that submit but never arrive.
Step 4: Security plugins, cookies and site URL mismatches
If you get the error within minutes of logging in, something is changing who WordPress thinks you are between loading the screen and submitting it.
Confirm it
Check the site addresses first, on the server:
wp option get home
wp option get siteurl
There are no placeholders. Good output is two addresses with the same scheme and host, such as https://www.example.com for both, matching the address in your browser. If one says www and the other does not, or one is http, your login cookie may not be sent with some requests, so WordPress treats you as a different user and the nonce fails. The login loop guide covers fixing cookie and URL mismatches.
Next, rule out security plugins that limit sessions, force logout, change the login URL or rename cookies. Deactivate them one at a time:
wp plugin deactivate PLUGIN_SLUG
PLUGIN_SLUG is the plugin folder name from wp plugin list. Good output is Success: Deactivated 1 of 1 plugins. Test after each one. If the error stops, check that plugin’s session timeout and concurrent login settings, then reactivate it with wp plugin activate PLUGIN_SLUG.
Salts that keep changing
Some security plugins can rotate the keys in wp-config.php on a schedule. Every rotation logs everyone out and voids all nonces. If errors line up with those times, lengthen or disable the schedule.
Step 5: A plugin that shortens or mishandles nonces
If only one plugin screen or one form fails, the plugin may shorten nonce life or check the wrong action. Search the code on the server:
grep -rn "nonce_life" wp-content/plugins wp-content/themes wp-content/mu-plugins
There are no placeholders. -r searches folders recursively and -n prints line numbers. Good output is nothing. A match shows code that changes the lifespan; if it returns a small number of seconds, nonces expire far sooner than normal. Report it to the plugin author. If nothing matches and only one screen fails, test with all other plugins deactivated on a staging copy and report the bug with steps to reproduce.
Advanced: when exactly does a nonce expire?
Because a tick is nonce_life / 2 and the counter is ceil( time() / 43200 ) with the default lifespan, tick boundaries fall at fixed clock times: 00:00 and 12:00 UTC every day. A nonce stays valid through its own tick and the next one. So:
- A nonce created at 11:59 UTC is valid until 00:00 UTC that night, about 12 hours.
- A nonce created at 12:01 UTC is valid until 12:00 UTC the next day, almost 24 hours.
This explains a common pattern: cached forms and old tabs often start failing just after midnight or noon UTC. You can check the current tick on the server:
wp eval 'echo wp_nonce_tick(), " ", gmdate( "Y-m-d H:i:s", wp_nonce_tick() * 43200 ), PHP_EOL;'
There are no placeholders. The first value is the current tick number; the second is the UTC time when this tick ends. Nonces made now remain valid until 12 hours after that time. If a plugin filters nonce_life, the 43200 in this command no longer matches, and the output will look wrong compared with the real expiry; that mismatch is itself a clue.
For logged-out visitors the user ID is 0 and the session token is empty, so everyone receives the same nonce for a given action during a tick. That is what lets a page cache serve forms safely, but only if the cache lifespan stays under 12 hours, the shortest possible nonce life.
When to contact your host
Open a ticket if you cannot raise PHP limits yourself, if Site Health shows old values after you changed them, or if a server cache is caching wp-admin. Send:
- The exact action that fails and the URL of the screen.
- The UTC time of a failed attempt.
- The PHP error log line, especially any “POST Content-Length” warning.
- Current upload and post size values from Site Health, and the file size you need.
- Response headers from the curl checks in Step 3.
If you are locked out of wp-admin entirely, go to the Emergency Room.