Why plugin sprawl is an operational risk
WordPress has no separation of rights between plugins. Every activated extension runs in the same PHP process as the core, with the same database rights, the same file system rights and access to every hook that WordPress offers. There is no sandbox, no capability boundary between two plugins and no declaration of which data an extension may touch. A plugin whose only job is to render contact forms can read orders, create user accounts and write files. This is not carelessness in the core, this is the extension model of WordPress, and it is one of the reasons for its wide use. The operational consequence is uncomfortable: the security level of an installation is not the average of all plugins, but the level of the weakest one. Thirty plugins are therefore thirty decisions of trust.
Two figures from the Patchstack report for 2025 belong in every decision document. First: 46 per cent of the reported vulnerabilities had no patch from the vendor at the time of public disclosure. Whoever defines security as “we update promptly” has no answer for almost half of the cases, because there is nothing to update. Second, the speed: the median time to the first exploitation attempt, weighted by observed attack intensity, is five hours. Among the heavily exploited vulnerabilities, 20 per cent were attacked within six hours, 45 per cent within 24 hours, 70 per cent within seven days. A monthly update window is not a protection concept against automated mass scans. The first measure is therefore not faster updating, but a shorter list.
Deactivating does not clear anything up. A deactivated plugin is still code in the directory wp-content/plugins, and whether one of its files can be reached and executed directly through the URL depends only on whether the author protected it against direct calls. The usual check for ABSPATH at the beginning of a file is a convention, not an obligation. Deactivated plugins also continue to be offered updates, which lengthens the list in the update window without anyone gaining from it. If an extension is not needed, it is deleted. Something usually stays behind in the process, though: only an author who supplies an uninstall routine through uninstall.php or register_uninstall_hook really clears away their options, tables and metadata. The rest leave data remnants that nobody can assign to anything later.
The third cost centre is overlap. Two caching plugins that overwrite each other's headers. An SEO plugin that sets meta information, and a theme that outputs the same information a second time. Three extensions that each take over part of the cookie banner. Such pairs cost nothing in licence fees and a lot in fault finding, because the behaviour depends on the load order and can change after every update. On top of that comes the ongoing effort: every plugin is an update candidate, a compatibility risk at every major release and an entry that somebody has to rule out when there is a fault. Twelve plugins can be switched through individually in one morning. Forty cannot.
The plugin inventory: what we record and what follows from it
The inventory is a table, not a feeling. Recorded per extension: name and slug, origin (the directory on wordpress.org, commercial vendor, built in-house, unknown), installed and available version, date of the last update by the author, order of magnitude of active installations, the value from the “Tested up to” field, open security reports, licence and term status for commercial plugins, and, in one sentence, the purpose: which function in the frontend or in the editorial work depends on this extension. Together with the question that most often remains unanswered: is it even being called any more? We fetch the basic data through WP-CLI, the rest through the directory, the change history of the vendor and a look into the code. Comparing the files against the version in the directory with wp plugin verify-checksums additionally shows whether someone has edited a third-party plugin.
When the maintenance state is assessed, the warning in the directory is a weak signal in both directions. The yellow box “This plugin hasn't been tested with the latest 3 major releases of WordPress” appears when the “Tested up to” field of the readme.txt does not name any of the last three major versions. It says nothing about faults, and it can be cleared by changing one line in the readme.txt without a single line of code being touched. Conversely, a missing warning does not mean that anyone is paying attention. Four other things are more reliable: the date and content of the last actual code change, how many support requests are unanswered, how the author reacted to the last reported vulnerability, and whether there is a named person or company behind it at all. For commercial plugins there is the additional question of whether the licence is still running — with most vendors an expired term means no updates, not even security-related ones.
The inventory turns into a decision per row, with four possible outcomes. Keep: the extension is maintained, is needed, has no duplicate. Replace: the function is needed, but the vendor is the weaker part of the construction, or two plugins do the same thing and one is enough. Remove: the function is no longer needed, or the core can do it itself by now — that happens more often than the list suggests. Build in-house: the function is business-critical, and there is no vendor you would want to entrust it to. Every row gets a reason and an effort estimate, so that the list stays usable without us. And we clear up one expectation beforehand: clearing up usually does not mean halving. Most plugins stay. The gain is that afterwards you can say why, for every single one.
When the vendor stops — and when the exit becomes expensive
An orphaned plugin does not announce itself. If an extension is closed on wordpress.org, the directory page only carries the note “This plugin has been closed as of [date] and is not available for download”, but nothing changes in your installation: there is no update, so WordPress shows the plugin as current. The reason for the closure is only disclosed after 60 days, and even then only as a rough category — wordpress.org does not give details. The category decides how urgent the case is. “Author Request” means: there is time for an orderly replacement. “Merged Into Core” means: the plugin can go, the function is now in WordPress itself. “Guideline Violation” or “Licensing/Trademark Violation” means: check what is actually running there. “Security Issue” means: act today. Because none of this appears in the administration, comparing the installed slugs against the directory belongs in the maintenance routine — otherwise you only notice it when somebody has found the vulnerability.
With an orphaned plugin there are four ways forward, and none of them is waiting. Replacement by a maintained alternative is the normal case, but it costs data migration if the old extension used its own tables or meta fields. Continuing the maintenance within your own area of responsibility is permitted for GPL-licensed plugins and is sometimes sensible, but it moves the responsibility entirely to you: a fork that nobody maintains is only a renamed orphaned plugin. Taking over the plugin in the directory is the orderly path if the author cooperates — the author can transfer the rights personally, and anyone who wants to hand a plugin over marks it with the tag adopt-me. If there is no answer, a procedure runs through plugins@wordpress.org: submit revised code, the plugin team writes to the author, and after 30 days without a reaction the handover can take place. With a plugin that is already closed, this path is considerably more difficult. That leaves new development of the function, if it is too important to depend on third-party code. Which path is the cheapest is decided by the scope of the function, not by habit.
The special case is page builders, because there it is not only the function that depends on the plugin, but the content. Elementor stores the page structure as JSON in the postmeta field _elementor_data; the field _elementor_css holds not content, but the CSS generated from it. The field post_content often stays empty or rudimentary. The global settings are also not stored as an option, but in a post of their own — the kit — which the option elementor_active_kit in wp_options only refers to. WPBakery stores its structure as shortcodes in post_content, which without the plugin leaves visible shortcode text instead of content. The block markup of the core editor, by contrast, sits in post_content as HTML with comments and stays readable markup even without anything else. The difference is not a matter of taste, but a calculation of costs for the day on which the builder is changed or discontinued: with the block editor you migrate markup, with a JSON builder you rebuild pages, page by page, with editorial acceptance.
This is why we assess a page builder that is in use instead of condemning it. If Elementor or WPBakery has worked for years, if the editorial team works with it and the vendor is active, there is no reason to tear the whole thing out in a maintenance project — the effort is out of all proportion. The exit makes sense on exactly two occasions: when the design is going to be replaced anyway and the content is restructured in a relaunch, or when the builder itself becomes a risk because it blocks a major update. What we deliver beforehand is the figure that is missing: how many pages are actually stored in the builder format, how many of them are plain text pages that can be transferred with reasonable effort, and which templates have to be rebuilt by hand.
Custom development: when it is right and when it is not
A plugin of your own is the right answer when the function concerns your business and not the internet in general. Four cases occur regularly. First, rules that apply only in your company: price tiers by customer group, approval processes, discount logic, configuration rules for products that need explanation. Second, interfaces to your systems — inventory management, ERP, PIM, CRM — because there the data model of the other side decides, and not the data model of a plugin vendor. Third, functions that have to be clean in terms of data protection and where nobody can explain where a ready-made plugin sends the data. Fourth, the case in which three extensions plus glue code are needed to achieve what one manageable plugin of your own does directly: then custom development is not the expensive option, but the cheap one — and the maintainable one.
For standard tasks the answer is no, even if that works against the order. Forms, backups, caching, image compression, metadata for search engines, redirects, two-factor authentication, cookie consent: there a maintained plugin has collected edge cases over years that you do not know and that we do not know straight away either — character set problems in attachments, mail servers that answer differently from the documentation, browsers that treat file fields in their own way. Anyone who asks us to develop a contact form does not get a cost estimate from us, but the names of two plugins. This works in the other direction as well: if a custom development does not pay off over the expected period of use, we say so before the quotation and not in the second project phase. A plugin that covers a function which is available ready-made is a maintenance burden without value in the long run.
What we build is built so that updates keep working. In concrete terms this means: no changes in the core, no changes in third-party plugin or theme directories, no patches that are overwritten by the next update. Extension happens through hooks and filters — add_action and add_filter — and through the registrations provided for blocks, REST routes, custom post types and meta fields. Everything of our own sits in a plugin of its own with its own prefix, so that function and option names do not collide with third-party code; theme adjustments belong in a child theme, not in the original. In addition there are the mandatory parts that cannot be seen from outside: capability checks and nonces for every writing action, prepared statements for everything that goes into the database, dependencies through the header “Requires Plugins” available since WordPress 6.5 instead of through a check in the code, a run with Plugin Check before acceptance, an uninstall routine, and the source code in your repository. A plugin that blocks a WordPress update is a construction fault and not a feature.
What WordPress 7.1 breaks in custom-built extensions
WordPress 7.1 is released on 19 August 2026, in parallel with WordCamp US in Phoenix. First a clarification, because it stands wrongly in many check lists: React 19 is not coming with 7.1. The upgrade was reverted after incompatibilities had appeared between the old and the new React version, and in the way plugins include React. 7.1 stays on React 18.3; Gutenberg offers an experimental flag for testing React 19. Anyone who builds their check list around React migrations is checking the wrong thing. The relevant breaking points are in three other places, and they affect precisely the code that nobody is watching: custom meta boxes, custom blocks, custom columns in list views, small administration pages from earlier projects.
The first and most important change: from 7.1 onwards the post editor is always in an iframe — independent of the theme type, independent of the block API version of the registered blocks and independent of that of the blocks in the content. Until then there was an exception: in 7.0 the block API version of the blocks actually used decided whether the editor was iframed, and before that the version of the registered blocks; block themes forced the iframe in any case. That is why many installations never saw the change. From 7.1 onwards there is no exception any more. What breaks is every piece of code that accesses the global document or window in the editor — that is, most of what reaches into the editor area with jQuery, MutationObserver instances on editor content, third-party scripts that are attached in the editor, and CSS that is meant to style content in the canvas through a global selector. The way forward is through an element in the canvas and its properties ownerDocument and defaultView instead of through the global objects; event listeners on the canvas are attached with useRefEffect and removed there as well. Styles have to be brought into the iframe through the registrations provided for it; CSS included globally no longer reaches the content.
The second change concerns the post list tables — and with them pages and custom post types as well. The leading th with scope="row" has moved from the checkbox column to the title column: the checkbox cell is now a td, and the title cell is a th that carries the title as its aria-label. For screen readers this is a gain, for extensions it is a breaking point in markup that had been essentially unchanged since 2010. Anyone who selects through the CSS class check-column in order to position a custom column, mark a row or attach an action menu now finds nothing; code that expects the title and the row actions inside a td breaks in the same way. Affected are custom columns in the post and page overview, list views of custom post types, JavaScript for bulk actions and all CSS that is calculated on the old column structure; anyone who has to serve both versions selects td and th together as a temporary measure. The third change is smaller and is noticed all the same: the property __next40pxDefaultSize has become ineffective, form elements render at a height of 40 pixels unconditionally. Anyone who laid out custom panels, sidebars or meta boxes for the earlier standard of 36 pixels — or worked with size="__unstable-large" — gets shifted layouts.
This is checked on a copy, not in the live system, and in this order: open the editor and read along in the browser console, use every custom block and every custom meta box once instead of only looking at it, click through every administration page with a list table, switch on WP_DEBUG with logging and read the log afterwards, and send custom plugins through Plugin Check. The reassuring part of this is the timing: WordPress has installed minor releases by itself since version 3.7, and plugins and themes on request since 5.5 — 7.0.2 of 17 July 2026 even arrived as a forced update, with critical security fixes. A major version step such as 7.1, by contrast, does not arrive on its own. You set the date. But that also means: whoever does not set it only postpones it — and then checks under time pressure against 7.2, which according to the schedule published on 12 August 2026 is due to be released on 9 December 2026.