Plugin Audit & Cleanup
Plugin Audit & Cleanup
A documented playbook for plugin audit and cleanup work on a client WordPress site. This is hourly technical support work, not a productized engagement — the playbook exists because the process is predictable enough to be worth standardizing across team members and clients.
When this work comes up
Triggers vary but the most common are:
- A host (WP Engine, Kinsta, Cloudways) flags performance issues and is upselling hardware/plan upgrades
- A care plan client is experiencing slowdowns, 504 timeouts, or database bottlenecks
- A new client is being onboarded and has obvious plugin bloat from years of accumulation
- An hourly support ticket reveals plugin conflicts or unused plugins consuming resources
- A site audit (for any reason) surfaces 30+ active plugins with no clear ownership
The agency owner makes the call on whether to run this playbook. The developer never starts it unilaterally based on what they see during other work — surface findings to the owner and let them decide whether it becomes a scoped engagement.
What the work does
Five things, in this order:
- Inventories every plugin currently active on the site and determines which are genuinely in use
- Safely removes plugins that aren't needed, in batches with observation windows
- Resolves any specific high-impact plugins identified during investigation (heavy tracking plugins, oversized log tables, abandoned analytics, etc.) by either removing them outright or replacing them with lighter alternatives
- Cleans up the database residue left behind by current and previously-removed plugins (orphan tables, options, postmeta, cron jobs)
- Hands the agency owner an engagement summary capturing what changed and what's recommended next
It is not a full performance optimization. We're not refactoring themes, tuning caching layers beyond obvious misconfiguration, or redesigning information architecture. Performance improvements happen as a side effect of removing bloat, but they're not the headline.
It is not open-ended. We work within the scope the owner quoted the client, surface scope creep, and stop at the engagement boundary.
Who does what
| Role | Responsibilities |
|---|---|
| Agency owner | Sells the engagement, handles all client communication, makes judgment calls on architectural decisions, reviews and signs off on anything affecting scope or timeline. |
| Developer | Runs the engagement day-to-day. Owns investigation, staging testing, production execution, and database cleanup. Maintains the engagement state document. Escalates when the playbook says to. |
| Claude project assistant | Pair-works with the developer throughout. Suggests commands, interprets output, enforces methodology, catches mistakes. Does not talk to clients. |
Tools & AI assistance
Required tools
- SSH access to the host (WP Engine, Kinsta, Cloudways, generic VPS, etc.)
- WP CLI — primary investigation and execution tool
- Host control panel access for snapshots and staging environment creation
- Code editor for inspecting theme files and writing any replacement code
- The Claude project for guidance throughout (see below)
Installed on staging during engagement
- Query Monitor — request-level performance and query analysis
- WP Crontrol — cron job inspection
- WP Advanced Database Cleaner Pro — orphan detection and removal (Phase 6 only; PixelPress holds a Pro license)
Reference tools
- GTmetrix or PageSpeed Insights — before/after performance baseline
- Browser developer tools — visual inspection during staging testing
The Claude project
The team uses a dedicated Claude project for this work:
- Project name: WordPress Plugin Audit & Cleanup
- Account: pixelpressteam@gmail.com
- URL: https://claude.ai/project/019eb2af-4745-7605-94aa-792111af3cd3
The project is configured with a system prompt that establishes a senior-engineer-mentoring-junior dynamic, enforces the methodology described in this playbook, refuses shortcuts, and provides WP CLI commands and SQL queries throughout. The system prompt is maintained inside the project itself — if it needs updating, the agency owner edits it directly there.
Session pattern:
- Developer opens a new chat in the project at the start of work
- Developer states which engagement they're on, which phase, and pastes the current engagement state document
- Claude reviews the state, confirms the plan for the session, asks any pre-flight questions
- Claude walks the developer through the next batch of work command by command, interpreting output as the developer pastes it back
- At end of session, Claude produces a state doc update snippet for the developer to paste into the master state doc
- Session ends
Don't try to run an engagement without the Claude project — the methodology assumes that interaction pattern.
When SSH isn't available (~20% of engagements)
Fall back to admin-level WordPress access + phpMyAdmin or equivalent DB access + FTP/SFTP for file inspection. The methodology still works but is slower. If a client can't provide SSH and the engagement involves more than ~30 active plugins, raise it with the owner before starting — the time estimate may need adjustment.
The engagement state document
Every engagement has a state document. It's a Google Doc the developer maintains throughout. It's the single source of truth for engagement progress and it's what gets pasted at the start of each Claude session.
See Engagement State Template for the structure. Copy it at the start of every engagement.
The developer updates this document after every session. The Claude assistant produces a markdown update snippet at the end of each session.
The seven phases
Phase 0 — Pre-flight
Mechanical, boring, critical. Before any technical work starts.
Checklist:
- Engagement brief received from owner (scope, hours, client constraints, deadlines)
- Access verified: SSH credentials work, host portal access works,
wp --inforuns successfully - Backup point confirmed fresh (host snapshot within last 24 hours, or take one now)
- Staging environment created or refreshed from current production
- Engagement state document created and populated with intake info
- Known-good plugin list from client documented in state doc
- Any client-specific constraints noted (page builder constraints, plugins not to touch, microsites or special templates to be careful around)
- Phase 0 marked complete in state doc
Do not proceed to Phase 1 until every item is checked. If any item can't be checked, escalate to the owner.
Phase 1 — Production reconnaissance (read-only)
Build a complete picture of current state without changing anything. All commands read-only. We're on production.
Run these in order, capturing output to a working notes file or pasting into the Claude session as you go.
Plugin inventory
wp plugin list --format=csv > plugins-inventory.csv
wp plugin list --status=active --format=count
wp plugin list --status=inactive --format=count
Theme inventory
wp theme list --format=csv
wp option get template
wp option get stylesheet
Autoloaded options analysis (often where the silent killer lives)
wp option list --autoload=yes --format=csv > autoload-options.csv
wp db query "SELECT option_name, LENGTH(option_value) AS size FROM wp_options WHERE autoload='yes' ORDER BY size DESC LIMIT 30"
Database table sizes
wp db query "SELECT table_name AS 'Table', ROUND(((data_length + index_length) / 1024 / 1024), 2) AS 'Size (MB)', table_rows AS 'Rows' FROM information_schema.tables WHERE table_schema = DATABASE() ORDER BY (data_length + index_length) DESC"
Cron job inventory
wp cron event list --format=csv > cron-events.csv
Content scale by post type
wp post list --post_type=any --format=count
wp post list --post_type=post --format=count
wp post list --post_type=page --format=count
# Plus any custom post types discovered
Multisite check
wp core is-installed --network
# If true, engagement is more complex — escalate before proceeding
WordPress and PHP version
wp core version
wp eval 'echo PHP_VERSION;'
By end of Phase 1, the state doc should have:
- Plugin counts (active/inactive)
- Top 10 autoloaded options by size
- Top 10 database tables by size (this is where heavy tracking or logging plugins surface as outliers)
- Total cron event count and any obviously orphaned ones
- Content scale by post type
- WP and PHP versions
Typical time: 1-2 hours including documentation.
Phase 2 — Plugin investigation
Work through unknown plugins one by one. Output is the tier classification in the state doc.
For each plugin not on the known-good list, investigate:
Theme code references
# From wp-content/themes:
grep -r "plugin_function_name\|plugin_class_name\|plugin_shortcode" .
Each plugin has identifiable function names, class names, or shortcodes. Check the plugin's main file for add_shortcode, add_action, exported functions, and search the theme for those references.
Shortcode usage in content
wp db query "SELECT ID, post_title, post_type FROM wp_posts WHERE post_content LIKE '%[shortcode_name%' AND post_status='publish'"
Run for each shortcode the plugin exposes.
Block usage (for Gutenberg sites)
wp db query "SELECT ID, post_title FROM wp_posts WHERE post_content LIKE '%wp:plugin-namespace%' AND post_status='publish'"
Widget usage
wp option get sidebars_widgets
wp option get widget_plugin-widget-name
Cron events owned by the plugin
wp cron event list | grep plugin_keyword
Database footprint
wp db tables | grep plugin_prefix
wp option list | grep plugin_prefix
Once investigation is complete for a plugin, classify into one of four tiers:
| Tier | Description | Action |
|---|---|---|
| A — Safe to remove | Zero theme references, zero shortcode/block usage in content, no critical cron jobs, no dependencies. Examples: WP All Import after one-time migration, abandoned analytics plugins, leftover campaign plugins. | Stage Batch 1 |
| B — Likely safe, verify | Minor references that look vestigial, shortcode usage only in old drafts/trash, cron jobs that haven't fired meaningfully in months. | Stage Batch 2 |
| C — High-risk unknown | Found references but unclear if functional, custom plugins with no documentation, plugins tied to microsites or special templates, plugins integrating with external systems (CRMs, analytics, payment processors). | Escalate to owner for decision before action |
| D — Keep | Confirmed active dependencies. | Document where it's used; don't re-investigate next time |
Typical time: 3-5 hours depending on plugin count.
Phase 3 — Staging testing
All testing happens on staging. Production is not touched in this phase.
Refresh staging from production first. Host's copy environment feature does this — confirm it's recent before proceeding.
Install testing tools on staging only:
- Query Monitor
- WP Crontrol
Deactivate plugins in batches. Don't deactivate everything at once — you won't know which deactivation caused which symptom.
- Batch 1: All Tier A plugins (lowest risk, highest confidence)
- Batch 2: Tier B plugins, in groups of 3-5
- Batch 3: Any Tier C plugins the owner has cleared for staging testing
After each batch, walk through:
- Homepage
- Main blog/news index
- A representative single post or article
- Donation/conversion pages (if applicable)
- Contact page
- Any microsite or special template pages flagged in engagement brief
- WordPress admin dashboard
- Common editor flows (creating a draft, editing media)
- Multilingual variants if applicable
- Mobile views via responsive mode
For each page, check:
- Does it render correctly visually?
- Does Query Monitor show new PHP errors or warnings?
- Does form submission still work? (test with dummy data)
- Are there broken images, missing scripts, or styling issues?
Document any breakages in the state doc. If a plugin's deactivation broke something, that plugin moves from Tier A/B to Tier D (keep) and we note why.
Typical time: 2-4 hours including testing.
Phase 4 — Production execution
Only plugins that passed staging testing get deactivated on production. Same batch structure as Phase 3.
Before each batch:
# Confirm we're on production
wp option get siteurl
# Take a host snapshot — usually manual via host portal
Then deactivate the batch:
wp plugin deactivate plugin-name-1 plugin-name-2 plugin-name-3
Spot-check critical pages immediately after. Same key pages as staging testing. Confirming changes behaved as predicted, not doing a full retest.
Document what was deactivated, when, and observed behavior.
Wait at least 24 hours before the next batch, ideally 48-72. This surfaces delayed issues — cron-dependent functionality that runs daily, background tasks, scheduled emails. If a problem appears, reactivate before continuing.
Do not delete plugins in the same session as deactivation. Deletion happens in Phase 6 or as a separate later task, after the full observation window has passed (default one week per batch, longer for any plugin with uncertain dependencies).
Typical time: 2-3 hours active work spread across 1-2 weeks calendar time.
Phase 5 — High-impact plugin resolution
Some plugins surface during Phase 1 and Phase 2 as outsized contributors to performance problems — a single plugin owning hundreds of MB of database tables, a tracking plugin with millions of rows, a logging plugin that hasn't been pruned in years, an analytics plugin doing heavy synchronous writes on every page request. These need their own resolution workflow because the answer isn't always "remove" — sometimes the client genuinely needs the functionality and a lighter replacement has to be designed.
This phase often runs in parallel with Phase 4 because it doesn't depend on the broader cleanup.
How to recognize a high-impact plugin during investigation:
- Owns a database table in the top 5 by size or row count
- Owns a heavy autoloaded option (>500KB)
- Owns cron jobs that fire frequently and take significant time to complete
- Generates slow query log entries
- Was specifically flagged by the host or by the client as a performance concern
Common examples (not exhaustive — every site is different):
- Heavy view/popularity tracking plugins (WordPress Popular Posts, Post Views Counter, similar)
- Activity log plugins that have accumulated years of entries (WP Activity Log, Simple History, Stream)
- Broken link checker–style plugins that scan and store findings indefinitely
- Old backup plugins still writing logs to the database
- Form plugins with massive entry tables (where the client may or may not still need the entries)
- SEO plugins with extensive redirect logs or 404 logs (some Yoast features, RankMath)
- Heavy analytics plugins logging synchronously to the database
- Visitor tracking or heatmap plugins storing per-session data
- Abandoned cart plugins that never prune old records
Investigation steps (adapt to the specific plugin):
# Identify the plugin's database tables and their sizes
wp db tables | grep plugin_prefix
wp db query "SELECT table_name, ROUND(((data_length + index_length) / 1024 / 1024), 2) AS 'Size (MB)', table_rows FROM information_schema.tables WHERE table_schema = DATABASE() AND table_name LIKE '%plugin_prefix%'"
# Find where the plugin's output renders in theme code
grep -r "plugin_function_name\|plugin_class_name" wp-content/themes/
# Find shortcode usage in content
wp db query "SELECT ID, post_title FROM wp_posts WHERE post_content LIKE '%[shortcode_name%' AND post_status='publish'"
# Find widget/sidebar usage
wp option get sidebars_widgets | grep -i plugin_keyword
# Check for block usage (Gutenberg sites)
wp db query "SELECT ID, post_title FROM wp_posts WHERE post_content LIKE '%wp:plugin-namespace%' AND post_status='publish'"
Worked example: WordPress Popular Posts
WPP is the most common case the team encounters and a useful concrete reference. It's a popularity tracking plugin that logs every pageview into wp_popularpostssummary and wp_popularpostsdata, neither of which gets pruned by default. On long-running sites, these tables routinely hit millions of rows and cause minute-long queries during nightly backups.
# Check the damage
wp db query "SELECT COUNT(*) FROM wp_popularpostssummary"
wp db query "SELECT COUNT(*) FROM wp_popularpostsdata"
# Find rendering
grep -r "wpp_get_mostpopular\|WordPressPopularPosts\|wpp-" wp-content/themes/
wp db query "SELECT ID, post_title FROM wp_posts WHERE post_content LIKE '%[wpp%' AND post_status='publish'"
wp option get sidebars_widgets | grep -i wpp
Decision tree — applies to any high-impact plugin. Present findings to the owner, get direction:
If the plugin isn't rendering anywhere on the site: deactivate, truncate or drop its tables after backup, delete plugin. Developer executes with owner's signoff. This is the cleanest outcome and surprisingly common for plugins that were once used and then quietly replaced by something else without being uninstalled.
# After host snapshot, on staging first, then production after observation:
wp plugin deactivate plugin-slug
wp db query "TRUNCATE TABLE wp_plugin_table_1"
wp db query "TRUNCATE TABLE wp_plugin_table_2"
# Or DROP TABLE if removing the plugin entirely and we're confident
If the plugin is rendering something the client needs: stop. This becomes a senior decision. Replacement strategies vary by plugin type — editorial curation via ACF, precomputed values stored in post meta, offloading to a third-party service, a lighter alternative plugin, custom theme code. Owner makes the architectural call and either takes the work or assigns it back to the developer with specific direction.
Typical time:
- Removal path (plugin not rendering): 1-2 hours
- Replacement path (custom code or migration to lighter alternative): 4-8+ hours depending on complexity
Phase 6 — Database cleanup
Run only after plugins from Phase 4 have been deleted (not just deactivated) and observation windows have passed. Running it during the observation period will destroy data for plugins we're still watching.
Install WP Advanced Database Cleaner Pro on staging first.
Run discovery only — no cleanup yet.
The plugin identifies:
- Orphan database tables (left by previously-removed plugins)
- Orphan options in wp_options
- Orphan postmeta and usermeta
- Orphan cron events
- Autoload candidates (options currently autoloaded that probably shouldn't be)
Review every category before any removal:
For orphan tables — some "orphan" tables are custom (built by a previous developer outside any plugin), not orphans. Every flagged table needs a sanity check: does the table name pattern match a plugin we removed? Is there evidence the table was custom-built? When in doubt, leave it alone.
For autoload changes — only flip autoload to "no" on options that are clearly orphan or clearly oversized (a 1MB+ option autoloaded on every request is almost always wrong). Mass-flipping autoload can break plugins that legitimately need their settings loaded early.
Execute cleanup on staging. Test the site afterward — homepage, key flows, admin. Confirm nothing broke.
Take production snapshot, execute on production.
Document:
- Tables removed (count, total size)
- Options removed (count)
- Postmeta/usermeta removed (count)
- Cron events removed (count)
- Database size before and after
These numbers go in the engagement summary.
Typical time: 2-3 hours.
Phase 7 — Wrap-up
Generate the engagement summary using the Client Summary Template. Update the state doc to reflect completion. Document any recommendations for follow-on work.
Recommendations typically include things noticed but not addressed:
- Page builder bloat (Divi, Elementor, etc.)
- Theme inefficiencies surfaced during investigation
- Caching configuration issues
- Image optimization opportunities
- Plugins kept but with better alternatives available
- Future plugin governance suggestions
These become potential follow-on work for the owner to discuss with the client.
Typical time: 1 hour.
Total engagement hours
Realistic estimate for a typical mid-complexity engagement (35-50 active plugins, single WordPress install, no multisite, accessible host):
| Phase | Hours |
|---|---|
| 0 — Pre-flight | 1 |
| 1 — Production recon | 1-2 |
| 2 — Plugin investigation | 3-5 |
| 3 — Staging testing | 2-4 |
| 4 — Production execution | 2-3 |
| 5 — High-impact plugin resolution (removal path) | 1-2 |
| 5 — High-impact plugin resolution (replacement path, if needed) | +4-8 |
| 6 — Database cleanup | 2-3 |
| 7 — Wrap-up | 1 |
| Total (removal path) | 13-21 hours |
Engagements with multisite, very high plugin counts (60+), or complex microsite architectures can run 25-35 hours. Engagements without SSH access add 25-50% to investigation phases.
Hard rules
These never bend, regardless of time pressure or client requests:
- Staging first, always. Any change touches staging first. Production changes only after staging is tested and observed.
- Deactivate before delete. Plugins get deactivated, observed for the agreed window (default one week), then deleted. Never delete in the same session as deactivation.
- Snapshot before production changes. Every batch of production changes is preceded by a fresh host snapshot. No exceptions.
- Read-only first. Phase 1 is entirely read-only. No deactivations, no destructive queries, no file edits.
- No destructive SQL without explicit confirmation. Any
DROP,DELETE,TRUNCATE,ALTERrequires confirming environment, fresh backup, and exactly what will be affected — stated out loud, every time. - Don't touch what you can't identify. Unclear plugins stay. The cost of keeping a plugin is small; the cost of breaking client functionality is enormous.
- No client communication without explicit instruction. Developer doesn't email clients. Escalations go to the owner.
Common gotchas
The "orphan" trap. A plugin that's been deactivated but not deleted will have its data flagged as orphan by Advanced Database Cleaner. Never run cleanup operations during the observation period.
WPML and translation tables. Sites with WPML have additional plugin-prefixed tables (wp_icl_*) that should generally be left alone. Don't classify them as orphans.
ACF custom tables. If the client uses ACF Pro with custom database tables configured for field storage, those tables won't be tied to a recognizable plugin signature. Don't classify them as orphans.
Custom mu-plugins. Plugins in wp-content/mu-plugins/ are must-use plugins. Always active, often custom-built, frequently undocumented. Investigate carefully and surface findings to the owner rather than treating them like standard plugins.
Multilingual sites with separate content. On WPML or Polylang sites, plugins that render content (related posts, popular posts, taxonomies, etc.) may behave differently per language. Test all language variants during staging testing.
Cache plugins that don't get along. Multiple caching layers (host cache + WP Rocket + CDN) can mean cache doesn't invalidate immediately after changes. Always purge cache manually after changes during testing.
Form plugins with stored entry data. Gravity Forms, Ninja Forms, and similar store entries in their own tables. Never deactivate a form plugin without checking whether there's stored entry data the client may still need.
Cron-dependent functionality firing daily. Some integrations sync once a day, send digest emails on schedule, or do background maintenance overnight. Issues from deactivating a plugin with this behavior may not show up for 24+ hours. This is why we wait between batches.
Wordfence and other security plugins. When the host has its own WAF (like WP Engine) and the PixelPress care plan provides WAF coverage, redundant security plugins may be safe to remove. Always confirm with the owner — security removal is high-stakes and there may be specific configurations the client relies on.
Host-specific notes
WP Engine
- Snapshots are taken via the User Portal → environment → Backups. Wait for confirmation before proceeding.
- Staging copies are made via the User Portal → environment → "Copy environment". Refreshing staging from production overwrites the staging database and files entirely.
- SSH access uses the format
environment@environment.ssh.wpengine.neton port 22. - WP CLI is installed and available on the WP Engine SSH gateway.
- WP Engine has its own object cache and page cache — clear via User Portal → environment → Caching after major changes.
- WP Engine has a built-in WAF (Global Edge Security on most plans). If the site also has Wordfence active, this is often redundant.
- WP Engine has a list of disallowed plugins — confirm any replacement plugins aren't on it before installing.
(Other host appendices to be added as needed.)
Escalation triggers — when to stop and ping the owner
- Engagement brief is unclear or missing information needed to proceed
- Site is on multisite and that wasn't expected
- Evidence of malware, hacking, or serious misconfiguration outside engagement scope
- Custom mu-plugin or custom theme code that does something unfamiliar
- Plugin appears load-bearing for client business logic (custom checkout/donation/booking flows, payment processor integration, CRM sync, etc.)
- Production behavior differs from staging predictions
- Error during production change that doesn't resolve in 30 minutes
- Client contacts the developer directly
- Engagement appears to be growing beyond scope
- About to make any architectural decision (replacement plugin choice, custom code design)
- Phase 5 (high-impact plugin resolution) — escalate after investigation, before execution, every time
When escalating, write a clear summary for the owner: situation, options, recommendation. The Claude project assistant will help frame the escalation.