All posts
WordPressMay 6, 2026|7 min read

The rsync Exclude Trap That Breaks WordPress Plugin Migrations

Using --exclude='cache/' in your rsync command? That pattern matches at every directory level and silently deletes plugin source code. Here's what happened during a real migration, why it broke two plugins, and the correct exclusion list you should use instead.

S

Showrav Hasan

WordPress & Infrastructure Engineer

WordPressLinuxMigrationrsyncDevOps
The rsync Exclude Trap That Breaks WordPress Plugin Migrations

TL;DR

When using rsync to migrate WordPress files, never use bare directory names in your --exclude patterns:

WrongCorrect
--exclude='cache/'--exclude='wp-content/cache/'
--exclude='upgrade/'--exclude='wp-content/upgrade/'
--exclude='temp/'--exclude='wp-content/temp/'

Bare patterns like cache/ match at every directory level, including plugin subdirectories like tutor-pro/cache/ and elementor/core/upgrade/. This silently strips plugin source code and causes fatal PHP errors on the destination.

Here is the recommended exclusion list and the full story of how I learned this the hard way.


Quick Reference: Safe rsync Exclusion List for WordPress

If you just need the correct command, here it is:

rsync -a \
  --exclude='wp-content/cache/' \
  --exclude='wp-content/upgrade/' \
  --exclude='wp-content/uploads/cache/' \
  --exclude='wp-content/wpvividbackups/' \
  --exclude='wp-content/updraft/' \
  --exclude='wp-content/ai1wm-backups/' \
  --exclude='wp-content/debug.log' \
  --exclude='wp-content/themes/.git/' \
  /source/public_html/ /destination/public_html/

Every pattern is scoped to wp-content/ so it cannot accidentally match plugin internals. Read on for why this matters.


What Happened

I migrated a WordPress site last week. File sync with rsync, database import with mysql, search-replace with WP-CLI. Standard stuff. The migration went smooth until I opened the site and hit a white screen.

PHP Fatal error: Uncaught Error: Class "Tutor\Cache\QuizAttempts" not found
in /home/myuser/public_html/wp-content/plugins/tutor/classes/Quiz_Attempts_List.php on line 69

A missing PHP class in the Tutor LMS plugin. My first thought was a PHP version mismatch. The source server was running PHP 8.3 and the destination was on PHP 7.4. I bumped the destination to 8.3.

Same error.

Then I checked if the class file actually existed:

find ~/public_html/wp-content/plugins/tutor-pro/ -path "*/Cache/QuizAttempts*"

Empty result. The file was simply not there. And that made no sense because the backup archive definitely had it. I had extracted and synced the files myself just minutes earlier.

So I went back and looked at my rsync command.

The Command That Looked Fine

Here is what I ran during the migration:

rsync -a \
  --exclude='.private/config.json' \
  --exclude='wp-content/themes/.git/' \
  --exclude='wp-content/wpvividbackups/' \
  --exclude='cache/' \
  --exclude='temp/' \
  --exclude='upgrade/' \
  ~/tmp_backup_restore/public_html/ ~/public_html/

The exclusions looked reasonable. I did not want to carry over cache files, temporary data, or the WordPress upgrade directory. These are all expendable directories that get regenerated automatically.

But there was a problem. Three of those patterns were way too broad.

How rsync Pattern Matching Actually Works

When you write --exclude='cache/', rsync does not interpret that as "exclude the cache directory at the top level." It matches cache/ as a component at any depth in the directory tree.

That means all of these get excluded:

wp-content/cache/                    ← intended (WordPress cache)
wp-content/plugins/tutor-pro/cache/  ← NOT intended (plugin source code)
wp-content/plugins/flyingpress/cache/ ← NOT intended (plugin internals)

The Tutor Pro plugin stores its QuizAttempts class inside a cache/ subdirectory within the plugin. My rsync command silently skipped that entire directory. The plugin files landed on the server without their caching classes, and the free tutor plugin tried to instantiate Tutor\Cache\QuizAttempts, which no longer existed.

Same thing happened with --exclude='upgrade/'. Elementor stores core upgrade manager classes in elementor/core/upgrade/. That directory got excluded too, and after I fixed the Tutor issue, Elementor threw its own fatal error:

PHP Fatal error: Uncaught Error: Class "Elementor\Core\Upgrade\Manager" not found
in /home/myuser/public_html/wp-content/plugins/elementor/core/experiments/manager.php

Two different plugins, same root cause: bare rsync exclude patterns matching plugin subdirectories that share names with WordPress temp directories.

The Double Red Herring

What made this tricky to debug was the layering of symptoms.

Red herring #1: PHP version. The source was PHP 8.3 and the destination was PHP 7.4. That is a legitimate compatibility gap, and recent versions of Tutor LMS do require PHP 8.0+. So when the first fatal error appeared, the PHP version looked like the obvious cause. But bumping to 8.3 did not fix it because the class file itself was missing from disk.

Red herring #2: Plugin version mismatch. The tutor (free) plugin expected a class from tutor-pro that was not there. This looked like a version mismatch between the two plugins. But the versions were fine. The class was simply in a directory that rsync had skipped.

Both red herrings were real issues in their own right (you should absolutely match PHP versions during a migration), but neither was the actual cause of the fatal error. The actual cause was three characters: cache/ vs wp-content/cache/.

The Fix

Scope every exclude pattern to its intended location:

rsync -a \
  --exclude='.private/config.json' \
  --exclude='wp-content/themes/.git/' \
  --exclude='wp-content/wpvividbackups/' \
  --exclude='wp-content/cache/' \
  --exclude='wp-content/temp/' \
  --exclude='wp-content/upgrade/' \
  ~/tmp_backup_restore/public_html/ ~/public_html/

The difference:

PatternWhat it matches
cache/Any directory named cache at any depth
wp-content/cache/Only wp-content/cache/ specifically

After re-running rsync with the scoped patterns, both plugins loaded cleanly. The site came up without errors.

Things I Do Not Exclude

  • Plugin subfolders with names like cache, temp, upgrade, backup. These are plugin source code, not expendable data.
  • wp-config.php. I handle this separately (backup the destination copy before rsync, then restore it after). If you want to understand what can go wrong with database configuration during migrations, check out my guide on fixing the "Error Establishing a Database Connection" error.
  • .htaccess. Some migrations need the source .htaccess rules. I review and merge manually.

Picking the right hosting provider also matters here. If your host manages migrations for you, you avoid these rsync pitfalls entirely. I wrote about how to choose the right web hosting if you are evaluating options.

How to Verify You Are Not Excluding Too Much

If you want to check what rsync would skip before actually running it, use --dry-run with -v:

rsync -av --dry-run \
  --exclude='cache/' \
  /source/public_html/ /destination/public_html/ \
  | grep "cache"

This will list every file and directory that matches your exclude pattern. If you see plugin paths in the output, your pattern is too broad.

You can also check after the sync by looking for known files:

find ~/public_html/wp-content/plugins/ -path "*/cache/*.php" -type f

If a plugin's PHP files are missing from a cache/ directory, you know the exclude pattern caught them.

The Takeaway

rsync's --exclude is powerful but its matching behavior is not intuitive. A bare directory name like cache/ looks like it targets one specific directory, but it is actually a wildcard that matches everywhere in the tree.

For WordPress migrations specifically, always prefix your exclusions with wp-content/ to scope them to the WordPress cache and temp directories. This keeps your plugin and theme source code intact while still skipping the large, regenerable files you do not need to transfer.

It took me one broken migration to learn this. Hopefully this saves you from the same mistake.


Frequently Asked Questions

What does rsync --exclude='cache/' actually match?

It matches any directory named cache at any level in the file tree, not just at the root. So wp-content/cache/, wp-content/plugins/some-plugin/cache/, and even wp-admin/includes/cache/ would all be excluded. rsync treats bare patterns without a leading slash as relative matches at any depth.

How do I exclude only the WordPress cache directory and nothing else?

Use the full relative path: --exclude='wp-content/cache/'. This tells rsync to only skip the cache directory that sits directly inside wp-content, leaving all other cache directories (including those inside plugins) untouched.

Can I use rsync --dry-run to test my exclude patterns before migrating?

Yes, and you should. Run rsync -av --dry-run --exclude='your-pattern' /source/ /dest/ and pipe the output through grep for your pattern name. The dry run lists every file that would be transferred or skipped without actually copying anything. If you see plugin files being excluded, your pattern is too broad.

Should I exclude wp-config.php during an rsync migration?

Do not exclude it in the rsync command. Instead, back up the destination's wp-config.php before the sync, let rsync overwrite it, then restore the backup. The destination's wp-config.php contains the correct database credentials, table prefix, and authentication salts for the new server. If you let the source config persist, the site will try to connect to the old database.

What WordPress directories are safe to exclude during migration?

These are safe to skip because WordPress regenerates them automatically: wp-content/cache/ (page cache), wp-content/upgrade/ (temporary update files), wp-content/uploads/cache/ (image optimization cache), and any backup plugin directories like wp-content/updraft/ or wp-content/ai1wm-backups/. Always scope the exclude to wp-content/ to avoid matching plugin internals.

Why did changing the PHP version not fix the "Class not found" error?

Because the PHP class file was physically missing from the server. The fatal error message said Class "Tutor\Cache\QuizAttempts" not found, which looks like a PHP compatibility issue. But the file containing that class (tutor-pro/cache/QuizAttempts.php) was never copied to the destination because rsync excluded it. The error would occur on any PHP version since the file simply did not exist on disk.

S

Written by Showrav Hasan

WordPress & Infrastructure Engineer with 3,500+ resolved incidents across Rocket.net, Hostinger, and NameSilo. I write about the troubleshooting workflows, server strategies, and engineering decisions behind real production support.

Need hands-on help with this?

I use these same strategies to resolve critical incidents for production WordPress sites.