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:

SettingWhat it controlsTypical default
upload_max_filesizeBiggest single file PHP accepts2M to 64M
post_max_sizeBiggest total POST body (must be at least the upload size)8M to 64M
max_execution_timeHow long PHP may run the request30 to 300 seconds
max_input_timeHow long PHP may spend parsing input60 seconds
memory_limitRAM per PHP process128M 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)

  1. Log in to cPanel and open MultiPHP INI Editor.
  2. Pick your domain from the dropdown.
  3. Set upload_max_filesize to 128M and post_max_size to 128M (or a bit above your largest file).
  4. Bump max_execution_time to 300 and max_input_time to 300.
  5. 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_value lines into .htaccess on 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 to wp-content/plugins/, then activate it from the dashboard.
  • WP-CLI: wp plugin install /home/user/plugin.zip --activate uses 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_size equal to or larger than upload_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.