Find out which plugin is slowing your site, by measuring it rather than guessing. The doctor times your own pages with each plugin briefly left out of its own internal test requests, then explains what each one costs in plain English.
Requirements
WordPress 6.2+, PHP 8.0+. The server must be able to request its own pages (the same loopback ability WP-Cron needs). Everything runs on your own server; no account, no cloud, no telemetry. Every screen, email and notice is translatable; community translations come from translate.wordpress.org.
Running a diagnosis
Tools > Speed Doctor opens the plugin. The Doctor tab walks you through two steps:
- Install the measurement loader. The scan cannot run without it (see below). One click installs it, and the step turns green.
- Run the diagnosis. This unlocks once the loader is in place. A preflight runs first: it checks that loopback requests work, that a page cache is not flattening the results, and that no background job is adding noise. Each check reads OK, Warning or Blocked; fix anything blocked and run again.
A scan sends a few hundred internal requests over several minutes and shows its progress and elapsed time. Once a page's own time is known, each further request to it may take up to three times that, never less than 10 or more than 30 seconds, so a slow page does not hold the diagnosis for half a minute per request. Leave the tab open; the browser warns you if you try to leave mid-scan. If the connection drops part-way, press Run diagnosis again and the doctor picks up the same scan where it left off. If you reload the page or come back to the Doctor tab within 15 minutes, it resumes the running scan by itself. A scan started by a schedule or WP-CLI is shown with its progress instead. Cancel diagnosis stops a running scan either way.
If your homepage redirects within the site (to a language or landing page, for example), the doctor follows up to three redirects and measures the page they lead to. A homepage that redirects to a different site, redirects more than three times or loops cannot be measured, and the preflight check says which.
Once you have a diagnosis, the Doctor tab also shows a summary of the latest one: how many plugins had high, medium or low impact or were inconclusive, and which ones need attention.
The measurement loader
Timing a plugin's absence means loading the site without it. A small helper file is placed in wp-content/mu-plugins, and it takes effect only for the doctor's own internal requests, which carry a cryptographic signature bound to the exact path and method being measured. Normal visitors and logged-in users always get the site exactly as configured. The file is installed only when you press Install loader on the Doctor tab or run wp speed-doctor scan. Its status (Installed, Out of date or Not installed) is shown on the Doctor tab, where you can also update or remove it, and it is deleted automatically when you deactivate the plugin. After a plugin update an installed loader is brought up to date by itself on the next request; a loader you removed on purpose is left alone, and automatic scans never install it either. A measurement request is a GET for the exact address and query string it was signed for, signed with the site's security keys as well as the stored secret; it expires within five minutes, tells page caches not to store the response, and cannot save rewrite rules or send a recovery-mode email. Before any scan, the preflight sends one signed request and checks that the loader answered it. If the page comes back without the loader's marker (the loader is missing, a page cache answered, or on a multisite network another site's deactivation removed the shared file), the scan is blocked with that reason rather than reporting every plugin as harmless. A signed page that loses the marker during a scan is counted as a failed request, never as a timing.
Reading the results
The Results tab opens with the impact counts, then lists verdicts worst first, each with a Low, Medium, High or Inconclusive badge and a measured range such as "adds about 180 to 260ms to page loads". Low-impact plugins are folded into a collapsible section so the ones worth acting on come first. A plugin can also be rated by the weight of the scripts and styles it ships; when that is what drives the badge, the verdict says how much it ships and how much of it is render-blocking. Ranges are medians with confidence bands taken from repeated interleaved timings, so they reflect real variation rather than invented precision. The noise band is never narrower than five percent of the page's own time, a cost needs at least four timings on each side and the timings with and without the plugin have to separate cleanly, so a difference smaller than the server's own jitter is never reported as a cost. A plugin that looks costly but is not yet certain when the planned timings are done gets four more cycles before the verdict is written. A plugin is rated Low only when the top of every measured range stays at or below 100ms, whether that is a small measured cost ("adds about 30 to 50ms") or no significant impact ("no significant impact, under about 20ms"); a plugin averaging 95ms with a range up to 140ms is Inconclusive, not Low. A small cost measured on one page is also Inconclusive when the other page (front end or wp-admin) was too noisy, and the verdict names that page. A page the scan set out to time but could not measure, for example wp-admin after it kept returning errors, stops any Low verdict the same way: the verdict is Inconclusive and says that page's cost was not measured. A page a scan deliberately leaves out, such as wp-admin on a scheduled or after-update scan, does not count against it. When the server is too noisy for a trustworthy answer, or too few timings could be collected, the verdict is Inconclusive instead of an invented number, and it is listed above the Low plugins. Script and stylesheet weight counts towards the badge only for what a plugin loads on front-end pages; wp-admin-only assets never count as shipped to visitors. Front-end and wp-admin are measured separately, because plugins that are cheap for visitors are often expensive in the dashboard.
Dependency groups
Some plugins cannot be loaded without another one and will fatal if separated, such as a WooCommerce extension without WooCommerce. When a plugin declares that dependency, the doctor measures the group as a whole, labelling the result so you know the cost is shared. A plugin that depends on several others joins one group with all of them.
If the site keeps returning a server error whenever a plugin is left out, something depends on it without declaring so. The same applies when a page that loads normally with every plugin active answers with a not-found or other error, or not at all, only while one plugin is left out (a language plugin that owns the homepage address, for example). The doctor retries in case the error was a passing hiccup, and reports that plugin as "could not be measured", with the HTTP status, only after three failures in a row, giving it an Inconclusive badge (or the badge its scripts and styles earn) rather than calling it harmless. Such errors count against that plugin only, and only while the page renders with every plugin loaded, so the page is still measured for every other plugin. A page is dropped only when it fails three times in a row with every plugin loaded as well; a rate-limited answer (HTTP 429) is blamed on nobody and tried again.
Assets and database
The Assets tab lists every script and stylesheet your pages load, attributed to the plugin that ships it, with sizes and render-blocking flags. A plugin whose PHP is fast can still send hundreds of kilobytes of JavaScript to every visitor. The Database tab reports autoloaded-option weight with probable owners, a common and largely invisible cause of slow requests, and marks the total as Healthy or Above 800KB. An option's owner is the active plugin whose folder name or text domain the option name starts with (Dragon Core plugins' short prefixes such as dsdp_ are recognised too); a name that matches no active plugin shows as Unattributed. Developers can add prefixes with the dragonspeeddoctor_option_prefixes filter (plugin folder => list of prefixes).
Before and after
Scan before a round of plugin updates and again afterwards to see exactly what changed: Results shows each plugin's change since the previous scan. The newest 20 diagnoses keep their full results (filterable with dragonspeeddoctor_retain_scans); older ones keep a compact summary for the History tab, and diagnoses older than a year are deleted. The two newest diagnoses from the Doctor tab or WP-CLI always keep their full results, so Results can still show and compare them however many automatic scans ran since. The newest few failed or cancelled scans are kept too.
Speed monitoring
Speed monitoring is off until you turn it on in Settings, because every automatic scan sends a few minutes of test requests to your own server.
- Automatic scans run weekly or daily, starting within 20 minutes after the quiet hour you pick (site time zone), whenever WP-Cron next runs. With "After an update" ticked, a follow-up scan also runs 10 minutes after a plugin, theme or WordPress update (not after installing a new plugin or theme); a run of updates shares one scan, booked no later than an hour after the first. A follow-up that finds another scan running waits 15 minutes, up to four times.
- How they run: in small steps through WP-Cron, so no browser tab is needed. On a quiet site the steps are as far apart as WP-Cron runs. A scan that stops before it finishes is retried an hour later, at most twice. Automatic scans time the front end only, since WP-Cron runs with no one logged in; a diagnosis from the Doctor tab times wp-admin as well. An automatic scan never installs the measurement loader: without it, the scan is skipped and the History tab and Site Health say why. A scan whose step chain cannot take its scan lock for 6 hours, or that waits 20 steps in a row without progress, is recorded as not finished and retried.
- Where they show: automatic scans are charted on the History tab. The Results, Assets, Database and Doctor tabs, and
wp speed-doctor results, keep showing the newest diagnosis run from the Doctor tab or WP-CLI, and Results compares it with the diagnosis before it. - Slowdown checks: each automatic scan is compared with up to four earlier automatic scans. A plugin counts as slower when its new cost is conclusive and clear of its earlier band; pages count as slower when the whole page time rose clearly above the earlier scans. One slow scan never sends anything: a confirmation scan is booked an hour later, and a slowdown is reported only for the pages and plugins both scans flagged against the same earlier scans. A confirmed level becomes the new normal, so it is not reported again. Manual and WP-CLI diagnoses are never compared or alerted on.
- Emails go to the addresses saved in Settings (the site's admin email until you save a list, up to ten) through WordPress's own
wp_mail(). They name what got slower, by about how much and since when, and the plugin, theme or WordPress update, or new plugin, that coincides with it, with a link to the History tab. An email that reaches nobody is tried again with the next automatic scan. "Send a test email" checks delivery; the last sends are listed under it. - History tab: weekly charts of front-end and wp-admin page times over 12 months (one point per scan for the first two weeks), with dashed markers where plugins, the theme or WordPress changed; the biggest movers between the newest scan and any earlier one you pick, and the plugins removed since; and every confirmed slowdown.
- Site Health (Tools > Site Health) reports monitoring off (a passed test, since off is the default), on schedule, overdue, blocked by the preflight with the reason, stopping before it finishes, or a slowdown confirmed in the last two weeks.
If the old Dragon Speed Doctor Pro add-on is still active, the built-in monitoring stays off so nothing is scanned or emailed twice, and the Speed Doctor screen asks you to deactivate the add-on.
Host-safe mode
A scan adds load while it runs. On a busy or resource-limited host, enable host-safe mode in Settings to space the requests out further. It takes longer and is gentler.
Behind a proxy or in a container
If the server cannot reach its own public URL, define DRAGONSPEEDDOCTOR_LOOPBACK_BASE in wp-config.php with an address it can reach, such as http://app-container or a private IP. Only the scheme, host and port are used: any path on the address is ignored, since each measured request keeps its own path (so subdirectory installs need nothing extra). The correct Host header is preserved so WordPress still routes normally. For safety the override is honoured only when the host is localhost, a loopback, private or link-local IP address written in full (dotted IPv4 or bracketed IPv6), or a hostname whose every DNS address is in one of those ranges. An IPv4-mapped IPv6 address is judged by the IPv4 address inside it, and only http and https bases are accepted. Anything else, including a name that does not resolve, is ignored, so measurement traffic never leaves your infrastructure.
Signed requests and administrator cookies only ever go to this site's own host and port; the scheme is not compared, so a site whose stored address is http while its pages are served over https is measured from WP-CLI and scheduled scans as well.
If the plugin could not create its scans table or store its signing secret on activation, an admin notice says so and setup is retried on each admin page load until it succeeds.
Multisite
Each site runs its own diagnosis from its own Tools screen. Plugins activated for the whole network are measured along with the site's own plugins when the diagnosis is run by a network administrator, by a schedule or from WP-CLI: a measurement request can then leave a network-activated plugin out just as it does a site plugin, and neither list can be saved while a measurement request is running. A site administrator's diagnosis covers the site's own plugins only, so it never loads the site without a plugin the network requires and never lists what the network runs. The measurement loader file in mu-plugins is shared by the whole network, so deactivating Speed Doctor on one site removes it for the others until it is installed again from the Doctor tab (WP-CLI scans reinstall it automatically).
WP-CLI
wp speed-doctor scan runs a full diagnosis from the command line and prints the verdicts, which makes it easy to script or schedule.
Uninstall
Deactivating removes the measurement loader and stops automatic scans; uninstalling clears their schedule on every site, whether or not the data is kept. Your scan history is kept on uninstall unless you opt in first, by ticking the delete-on-uninstall box in Settings. On a multisite network, uninstalling checks every site and removes the data of each site that ticked the box, leaving the others untouched.
For developers
dragonspeeddoctor_page_setfilters the pages timed (key => [url, auth]); its second argument is the scan source:manual,cli,scheduledorupdate.dragonspeeddoctor_scan_completefires with the scan id, its results and its environment (source, measured pages, versions).dragonspeeddoctor_tool_pluginslists plugins that are part of the tool: they are never measured and always loaded on measurement requests.dragonspeeddoctor_signed_requestfires only on verified measurement requests, with the plugin subset and any signed flags.dragonspeeddoctor_verdict_detailsfires under each verdict on the Results tab.dragonspeeddoctor_retain_scanssets how many diagnoses keep their full results (default 20); the rest keep a summary for a year.dragonspeeddoctor_scan_stale_afterfilters how many seconds a running diagnosis may go without activity before it is reclaimed as abandoned (default 900, clamped between 60 seconds and 24 hours); it receives the scan id and source.- Results carry
extendedandextra_requests(the extra cycles a diagnosis ran),throttled(whether the server rate-limited it) and, underassets,auditedandfailedpage counts.
