You pick a theme or plugin ZIP, hit Install Now, wait a few seconds, and WordPress throws this at you: "The link you followed has expired. Please try again." No stack trace, no log line, nothing helpful. It's one of the most vague errors in WordPress, but the cause is almost always the same boring thing: PHP refused the upload before WordPress even saw it.
Here's how to confirm that, fix it on cPanel or a VPS, and stop it coming back.
Symptom: what you'll actually see
- The message appears right after uploading a theme, plugin, or sometimes a large media import.
- Small files work fine; bigger ZIPs fail. A 2 MB plugin installs, a 40 MB page-builder theme doesn't.
- The upload bar sits there for a while, then the page reloads with the error.
Cause: why WordPress says "expired"
When a form post is bigger than PHP's post_max_size, PHP throws away the whole request body. That includes the hidden security nonce WordPress sends with the upload form. With no nonce, WordPress assumes your session link is stale and shows the "expired" message. The wording is misleading. Nothing expired.
The usual suspects:
| Setting | What it controls | Typical default |
|---|---|---|
upload_max_filesize | Biggest single file PHP accepts | 2M to 64M |
post_max_size | Biggest total POST body (must be at least the upload size) | 8M to 64M |
max_execution_time | How long PHP may run the request | 30 to 300 seconds |
max_input_time | How long PHP may spend parsing input | 60 seconds |
memory_limit | RAM per PHP process | 128M to 256M |
Less often, the culprit sits in front of PHP: an Nginx client_max_body_size limit, a ModSecurity rule, or Cloudflare's upload cap on the free plan (100 MB).
Fix 1: raise the limits in cPanel (easiest)
- Log in to cPanel and open MultiPHP INI Editor.
- Pick your domain from the dropdown.
- Set
upload_max_filesizeto128Mandpost_max_sizeto128M(or a bit above your largest file). - Bump
max_execution_timeto300andmax_input_timeto300. - Click Apply, then retry the upload.
If you'd rather edit files, put this in a .user.ini in your site root (works with PHP-FPM and CGI handlers):
upload_max_filesize = 128M
post_max_size = 128M
max_execution_time = 300
max_input_time = 300
memory_limit = 256M
Give it up to five minutes. PHP caches .user.ini and rereads it on a timer, so an instant change isn't guaranteed.
Don't paste
php_valuelines into.htaccesson a PHP-FPM account. That doesn't just fail quietly, it causes a 500 error.
Fix 2: on a VPS with PHP-FPM
Find the loaded config first so you edit the right file:
php -i | grep -E "Loaded Configuration|upload_max|post_max"
Edit the php.ini it reports (commonly /etc/php/8.3/fpm/php.ini on Ubuntu or /etc/php.ini on AlmaLinux), change the values from the block above, then reload:
sudo systemctl reload php8.3-fpm
# AlmaLinux / Rocky
sudo systemctl reload php-fpm
Remember that the CLI and FPM can use different ini files. Check the web view with a quick phpinfo() page, then delete that page straight away.
Fix 3: Nginx in front of PHP
If your server block has no explicit limit, Nginx defaults to 1 MB, and you'll see a 413 rather than the WordPress message. But if you've set a limit lower than PHP's, uploads will die early. Match them:
server {
client_max_body_size 128M;
...
}
Then run sudo nginx -t && sudo systemctl reload nginx. If you get a 413 specifically, our guide on fixing 413 Request Entity Too Large covers it in more depth.
Fix 4: skip the browser upload altogether
Sometimes the quickest route is not to fight the limits. Two reliable alternatives:
- File Manager or SFTP: upload the unzipped theme to
wp-content/themes/, or the plugin towp-content/plugins/, then activate it from the dashboard. - WP-CLI:
wp plugin install /home/user/plugin.zip --activateuses no HTTP upload, so no limits apply.
Still failing? Rule out the extras
- Cloudflare: the free plan caps request bodies at 100 MB. For bigger files, upload via SFTP or temporarily use the grey-cloud (DNS-only) record.
- ModSecurity: if the error pops up only for certain files, check the Apache error log for a rule ID and whitelist it from cPanel's ModSecurity tool.
- Plugin conflicts: some security plugins add their own upload size filters. Disable them for one test upload.
- Genuinely expired nonce: if the page was open for hours before you clicked, refresh and try again. This is the one case where the message is literally true.
Prevention
- Set sane PHP limits once, at 64M to 128M, when you build a site. Check them under Tools > Site Health > Info > Media Handling.
- Keep
post_max_sizeequal to or larger thanupload_max_filesize. If it's smaller, the smaller number wins. - Install big premium themes via SFTP by default. It's faster and doesn't depend on browser timeouts.
- Don't leave limits at 1 GB "just in case". Huge limits make it easier for runaway uploads to eat memory and disk.
Frequently Asked Questions
Does "the link you followed has expired" mean my session timed out?
Rarely. In most cases PHP dropped the upload for being too large, which removed the nonce, and WordPress reported it as an expired link. Check your upload limits first.
I raised upload_max_filesize but it still fails. Why?
Probably post_max_size is still lower, or you edited a php.ini the web server doesn't use. Confirm the live values in Site Health or via phpinfo, not just the CLI.
Can I fix this without access to cPanel?
Yes. Try a .user.ini file in the site root, or install the theme or plugin via SFTP. Alternatively, raise a ticket with us and we'll adjust the PHP limits for you.
Is it safe to set the limit to 512M?
It works, but it's rarely needed. Pick a value slightly above your biggest legitimate file, and don't forget memory_limit and the execution time should scale with it.
Does this also cause failures on media imports and backup restores?
Yes. Backup plugins, WooCommerce product imports, and XML importers all post big files, and the same limits apply to them.
