WordPress Plugin Development Mistakes to Avoid: A Code Audit Guide
The mistakes that hurt plugin projects most are not exotic. They are the same patterns recurring across teams and across years: a capability check that was forgotten on one AJAX endpoint, a query that does not use prepared statements because the developer thought the input was already safe, an admin notice that fires on every page load instead of only on the settings screen. None of these are difficult to fix. All of them are expensive to find after release, when a customer reports a symptom and the team has to retrace the codebase to identify the root cause.
This guide is built around the audit. Treat it as a list of patterns to look for when reviewing a plugin codebase, whether the codebase is one your team built or one your team inherited. The patterns are grouped by area, with the most common and most expensive failures first. At the end, a QA checklist consolidates the items into a single document you can run against a plugin before release.

Mistakes in security and input handling
Security mistakes appear in the audit results of nearly every plugin that has not been reviewed by someone specifically looking for them. The patterns are well-known, the fixes are documented, and the failures persist because reviews are often conducted by the same developer who wrote the code, who is not in the right frame of mind to find their own assumptions.
Missing capability checks on state-changing actions
Any action that changes state (saving a setting, creating a record, deleting a record, sending an email, calling a paid API) needs an explicit capability check that confirms the current user has the right permission. The check is straightforward (current_user_can() with the appropriate capability), but it is easy to forget on AJAX handlers, REST endpoints, and admin-post handlers because each of those routes has its own pattern and its own boilerplate. The audit step is to enumerate every state-changing action and confirm that each one has a capability check before the change is made.
Missing nonce verification on form submissions
A nonce protects against cross-site request forgery. Without it, a malicious site can trick a logged-in user’s browser into submitting a form to the plugin. The plugin sees the submission as coming from the legitimate user and processes it. The fix is to add a nonce field to every form and verify it before processing. The audit step is to grep the codebase for form processing handlers and confirm each verifies a nonce.
Unsanitized input flowing into database queries
Every value that originates from user input has to be sanitized before it is used. WordPress provides sanitization functions (sanitize_text_field, sanitize_email, absint, sanitize_key, and others) that handle the common cases. For database queries, prepared statements with $wpdb->prepare() are mandatory regardless of how clean the input looks. The audit step is to find every direct database query and confirm it uses prepared statements; the exception is queries with no variable input at all.
Unescaped output flowing into HTML or attributes
Every value that is rendered into HTML has to be escaped against the context it is rendering into. esc_html for HTML body text, esc_attr for attribute values, esc_url for URLs, esc_js for inline JavaScript contexts, wp_kses for HTML that is allowed to contain a limited set of tags. The mistake is to escape once at input time and assume the output is safe; output escaping is context-dependent and has to happen at the rendering point.
Storing sensitive data in plain text
API keys, OAuth tokens, and passwords for external services sometimes end up stored in the options table or in user meta in plain text. The plugin is then a tempting target for an attacker who gets database access. The fix is encryption at rest using a key that is not stored in the database, or delegation to a credentials manager. The audit step is to find every place the plugin stores a third-party credential and confirm it is encrypted.
Mistakes in performance and resource use
Performance mistakes tend not to show up during development because the development environment has small data, few users, and short response times. They emerge in production when the database has more rows than the developer tested with and traffic comes from real users rather than from refresh-by-refresh testing.
Uncached queries on every page load
A plugin that runs an expensive query on every page load adds the cost of that query to every page on the site. The fix is to cache the result, usually with a transient that expires on a reasonable schedule or invalidates when the underlying data changes. The audit step is to identify every query the plugin runs on front-end pages and confirm that queries with results that do not change per request are cached.
Hooks firing on requests where they are not needed
A common pattern is registering a hook globally when it only needs to fire on admin pages, on a specific admin page, or on the REST API. The result is the hook executing on every front-end request whether it is relevant or not. The fix is to register hooks conditionally, often with is_admin(), with a check on the current screen, or with a specific REST namespace condition. The audit step is to enumerate the hooks the plugin registers and confirm each one needs to fire as broadly as it does.
Synchronous remote requests in the request path
A plugin that calls an external API synchronously on a page load makes the page wait for the API. If the API is slow or unavailable, the page is slow or unavailable. The fix is to move external calls to background jobs (via WP Cron or a queue) or to cache the response aggressively. The audit step is to find every wp_remote_get, wp_remote_post, and similar call, and confirm it is either cached or backgrounded.
Loading large assets on every page
A plugin that enqueues a large JavaScript or CSS file on every front-end page imposes that cost on every page, even pages that do not use the plugin. The fix is to enqueue assets only on pages where the plugin’s content appears, which is straightforward when the plugin uses a shortcode or block (enqueue when the shortcode runs or the block renders) and more involved when the plugin contributes to layouts more broadly.
Database queries inside loops
The classic n+1 pattern: a loop iterates over results from one query and runs another query inside the loop for each row. The result is a query count that scales with the number of rows. The fix is to fetch the related data in a single batch query before the loop. The audit step is to find loops over post or term queries and confirm each iteration does not trigger an additional query.

Mistakes in compatibility and ecosystem behavior
Compatibility mistakes are different from security and performance mistakes in that they often do not manifest as errors. The plugin works on its own, fails subtly when combined with another plugin or theme, and the user reports the symptom as a vague “your plugin breaks my site.” The audit’s job is to look for the patterns that produce these symptoms.
Global namespace pollution
Functions, classes, and constants declared without a unique prefix collide with other plugins. The result is a fatal error when the second plugin tries to declare the same name. The fix is to namespace everything: prefix functions and classes with a plugin-specific identifier, use a PHP namespace, or use an autoloaded class structure. The audit step is to grep the codebase for top-level function and class declarations and confirm each is uniquely prefixed.
Modifying global state without restoring it
A plugin that changes $wp_query, the current post, the current user, or the current locale, and does not restore the original value, causes problems for code that runs after. The fix is to restore state explicitly, usually with the corresponding “reset” function (wp_reset_postdata, wp_reset_query, restore_current_blog, and others). The audit step is to find every place the plugin modifies global state and confirm the restoration.
Aggressive filter modifications
Filters that modify behavior globally (changing the main query, modifying every post’s content, altering every URL) are powerful but dangerous. They affect code that the plugin author did not write and cannot predict. The fix is to scope filter modifications as narrowly as possible: check the context, return early if the filter is being called for a case the plugin should not affect, and document why the filter is necessary. The audit step is to find every filter the plugin registers and confirm the scope is appropriate.
Assuming specific themes or page builders
A plugin that assumes the active theme produces a specific HTML structure, or that a specific page builder is being used, will fail on any site that does not match the assumption. The fix is to use WordPress’s standard APIs (the loop, template tags, content filters) rather than hard-coding theme-specific behavior. The audit step is to find theme-specific or page-builder-specific code and confirm there is a fallback for sites that do not match.
Ignoring multisite scoping
A plugin that works on a single site may break on multisite because it stores data globally when it should store per-site, or because it queries across sites when it should query within one. The fix depends on the plugin’s intended behavior: confirm whether settings are network-wide or per-site, whether data is scoped per site, and whether the plugin should activate network-wide or per-site. The audit step is to test the plugin on a multisite installation and confirm the scoping matches the intent.
Mistakes in user experience and admin behavior
The plugin works, the security is sound, the performance is acceptable, but the user has a hard time using it. These mistakes do not break sites but they create support load and damage the plugin’s reputation.
Admin notices that appear on every screen
A plugin that displays an admin notice on every admin page, regardless of relevance, irritates users quickly. Worse, a notice that cannot be dismissed makes the admin interface feel hostile. The fix is to show notices only on relevant screens, only when they are actionable, and always with a dismiss mechanism. The audit step is to find every add_action('admin_notices') handler and confirm the notice is scoped and dismissible.
Settings pages that do not save cleanly
A settings page that loses values when saved, validates inconsistently, or shows confusing error messages frustrates users. The fix is to use the Settings API or a comparable structured approach, validate each setting against its expected type, and show clear messages when validation fails. The audit step is to save the settings page with empty values, invalid values, and edge-case values, and confirm the behavior is reasonable in each case.
Missing or unhelpful error messages
A plugin that fails silently (the action doesn’t happen, no message appears) leaves the user wondering what went wrong. A plugin that shows a technical error message (a stack trace, a database error code) leaves the user wondering what to do. The fix is to show messages that describe what failed in user-friendly language and suggest what the user might do next. The audit step is to trigger each error path and confirm the resulting message is actionable.
Uninstall routines that leave residue
A plugin that creates database tables, options, scheduled events, and user meta should clean them all up when uninstalled. A plugin that leaves residue creates orphaned data that accumulates as the user installs and removes plugins over time. The fix is a thorough uninstall.php or register_uninstall_hook handler that removes everything the plugin created. The audit step is to install, configure, uninstall, and inspect the database for residue.
Quality assurance checklist for plugin audits
The checklist below consolidates the audit items into a single sequence. Run it against any plugin before release. Items are grouped by area, with each item phrased as a yes/no question. A “no” answer on any item means a finding to address.
Security
- Does every state-changing action verify a nonce?
- Does every state-changing action check a capability?
- Are all database queries using prepared statements?
- Is all user input sanitized appropriately for its type?
- Is all output escaped for its rendering context?
- Are third-party credentials encrypted at rest?
- Does the plugin avoid using
eval,unserializeon untrusted data, or shell execution? - Does the plugin avoid logging sensitive data?
- Does the plugin handle authentication failures gracefully without disclosing whether a username exists?
Performance
- Are expensive queries cached?
- Are hooks scoped to the contexts where they need to fire?
- Are external API calls cached or backgrounded?
- Are assets enqueued only on pages where they are needed?
- Are loops free of nested queries that scale with row count?
- Is the plugin tested against realistic data volumes?
Compatibility
- Is the global namespace clean (no unprefixed functions, classes, or constants)?
- Is global state restored after the plugin modifies it?
- Are filter modifications scoped narrowly?
- Does the plugin work with the two most common themes and page builders?
- Does the plugin behave correctly on multisite?
- Is the plugin tested against the current and previous WordPress major versions?
- Is the plugin tested against the current and previous PHP versions?
User experience
- Are admin notices scoped to relevant screens?
- Are admin notices dismissible?
- Do settings pages save cleanly with appropriate validation?
- Are error messages user-friendly and actionable?
- Does the uninstall routine remove all plugin data?
- Is the front-end output accessible (semantic HTML, ARIA where appropriate, keyboard navigable)?
- Is the front-end output responsive?
Code quality
- Does the plugin have automated tests covering critical paths?
- Does the plugin follow established coding standards (WordPress Coding Standards or PSR equivalents)?
- Are functions and classes documented with PHPDoc?
- Is the plugin’s public API (hooks, filters, REST endpoints) documented?
- Is the changelog kept current?
- Is the version number incremented appropriately for each release?

How to run the audit without paralysis
The checklist is long. Running every item against a plugin can take a full day for a small plugin and a week for a large one. A reasonable practitioner observes that the audit is most valuable when it is run consistently rather than perfectly. Three approaches let teams run audits without exhausting themselves.
The first is automation. Static analysis tools, security scanners, and linters can run the security and code quality items automatically. PHPCS with the WordPress Coding Standards ruleset catches a large fraction of the basic findings. Tools like PHPStan or Psalm catch type errors and dead code. WPScan and security scanners check for known vulnerable patterns. None of these replaces a manual review, but they reduce the manual review surface to the items the tools cannot check.
The second is risk-based prioritization. Not every item in the checklist matters equally for every plugin. A plugin that does not call external APIs does not need the backgrounded-API check. A plugin that does not have a settings page does not need the settings-page check. Trimming the checklist to the items that apply to the specific plugin reduces the audit time without reducing the audit value.
The third is incremental review. A plugin that is audited fully once and then audited only on changed code thereafter requires less effort over time than a plugin that is audited from scratch at every release. The discipline is to audit each pull request rather than each release, which catches issues at the change rather than at the aggregation.
Maintenance reminder: the audit is not a one-time event
The plugin that passes the audit at release will not necessarily pass the audit a year later, because WordPress changes, PHP changes, the plugin ecosystem changes, and the plugin itself changes. The audit needs a recurring schedule, not a single execution.
A reasonable cadence is a full audit on each major release, a focused audit on each minor release covering the areas that changed, and a quarterly review of the plugin’s dependencies for security advisories. The discipline is treating the audit as part of the ongoing operation of the plugin rather than as a release-time activity. A plugin that is audited regularly accumulates fewer surprises and ages better than one that is audited only when something has already gone wrong.
The plugin’s audit history is itself a useful artifact. A record of what was checked, what was found, what was fixed, and what was accepted as a known limitation gives the next reviewer context that would otherwise have to be reconstructed. The record does not need to be elaborate. A short document per audit, kept in the same repository as the code, is enough to make the audit history a real asset rather than a folder of forgotten reports.

Discussion
Join the Conversation
No comments yetNo comments yet
Start the discussion with a useful question, implementation note, or feedback that can help the next reader.