---
title: "The Plugin That Is Slowing Your Site, and How to Find It Without Guessing"
url: https://adityaarsharma.com/find-the-plugin-slowing-your-wordpress-site/
date: 2026-09-06
modified: 2026-09-03
author: "Aditya Sharma"
description: "Query Monitor names the plugin in one page load because of a db.php symlink that runs before WordPress boots. The method, and what the numbers mean."
categories:
  - "WordPress"
image: https://adityaarsharma.com/wp-content/uploads/2026/09/2d4c4834-8612-4436-82b8-73a3bae31a01_2800x3000-956x1024.png
word_count: 2069
---

# The Plugin That Is Slowing Your Site, and How to Find It Without Guessing

This is the readout that ends the argument, sitting in the WordPress admin bar:

`1.82s 28,840KB 0.0052s 19Q`
Page generation time, peak memory, total database query time, query count. Look at the third and fourth figures next to the first.

That page took 1.82 seconds to build and spent 5.2 milliseconds of it in the database, across 19 queries. The database is 0.3 percent of the problem.

Which means the standard advice, deactivate your plugins one at a time until the site gets faster, would have told you almost nothing here, after an hour of taking a production site down and up.

There is a tool that attributes the cost to a named plugin in one page load, and the reason it can do that is a piece of engineering worth understanding rather than just installing.

![The Query Monitor listing on the WordPress.org plugin directory, showing version 4.0.7, 200,000 plus active installations, and a header image of the Queries by Component panel attributing queries to wordpress-seo, core, woocommerce and edit-flow.](https://adityaarsharma.com/wp-content/uploads/2026/09/2d4c4834-8612-4436-82b8-73a3bae31a01_2800x3000-scaled.png)wordpress.org/plugins/query-monitor/, screenshot taken 2 September 2026. Version 4.0.7, 200,000+ active installations, tested to 7.0.4. The header image is the plugin's own screenshot of the Queries by Component panel.
On this page

- Why there is no crop button, and what Markup does instead- What iOS 26's Preview app does and does not do- Route one: rescan it, which is the best answer for scans- Route two: PDF to images, crop, back to PDF- Route three: when you only need one region of one page- What cropping a PDF this way actually costs you- Where Grabio fits- Cropping a PDF on iPhone: FAQ

## Why Query Monitor can name the plugin and your host's APM cannot

Every profiler can tell you a query was slow. Almost none can tell you which plugin issued it, because by the time a query reaches the database the call stack is gone.

### The db.php drop-in loads before WordPress

Query Monitor solves this before WordPress boots. From its own README, on the develop branch:

> In order to do a few clever things, Query Monitor symlinks a custom db.php into your WP_CONTENT_DIR which means it loads very early. This file gets included before the database driver is loaded, meaning this portion of Query Monitor loads before WordPress even engages its brain.
>
> github.com/johnbillion/query-monitor README, read 2 September 2026
That drop-in replaces the `wpdb` class with a subclass that records, for every single query, the full stack trace at the moment it ran.

WordPress supports exactly one `db.php` drop-in, and that slot is what buys you three things nothing else gets:

- Queries that run **before plugins load**, which is where a surprising amount of option loading happens.- The **calling function and the owning component** for each query, derived from the stack trace, which is how the Queries by Component panel exists at all.- The **result** of each query, so affected rows and error messages are shown rather than guessed at.

### What you lose when the symlink cannot be created

The consequence matters more than the mechanism. The README is explicit: if `WP_CONTENT_DIR` is not writable and the symlink cannot be created, Query Monitor still runs, but this extended functionality is gone.

You get query timings with no idea who is responsible, which looks identical to a working install until you go looking for the answer you installed it for.

So the first thing to check is not a setting. It is a file. Run `ls -la wp-content/db.php` and read what comes back.

A symlink pointing into the plugin is correct. A real file that is not Query Monitor's means another plugin already claimed the slot, which happens with some page-cache and monitoring plugins, and only one of them can win.

![The johnbillion/query-monitor repository on GitHub, the source of the db.php drop-in explanation.](https://adityaarsharma.com/wp-content/uploads/2026/09/9ef51068-ac29-4dc9-8604-972f39bc9c1e_2800x1900-scaled.png)github.com/johnbillion/query-monitor, screenshot taken 3 September 2026. The db.php symlink explanation quoted above is in this README.

## Setting it up so the numbers are real

Query Monitor 4.0.7, updated 20 June 2026, needs WordPress 6.2 or higher and PHP 7.4 or higher, per the plugin directory listing above. Install it, then add these to `wp-config.php` above the *stop editing* line:

- `WP_DEBUG` true and `WP_DEBUG_LOG` true.- `WP_DEBUG_DISPLAY` false, plus `@ini_set( 'display_errors', 0 )`, so errors go to the log rather than into the page.- `QM_HIDE_SELF` true, which excludes Query Monitor's own overhead from the numbers.

### SAVEQUERIES is set for you

You do not need to define `SAVEQUERIES`. Query Monitor does it for you, and you can read the line in its source:

`// collectors/db_queries.php
if ( ! defined( 'SAVEQUERIES' ) ) {
define( 'SAVEQUERIES', true );
}
if ( ! defined( 'QM_DB_EXPENSIVE' ) ) {
define( 'QM_DB_EXPENSIVE', 0.05 );
}`

### QM_DB_EXPENSIVE decides what turns red

The second constant is the one nobody knows about and it changes how you read the whole panel. **Query Monitor's definition of a slow query is 0.05 seconds.** Fifty milliseconds.

That is the threshold at which a row turns red and the warning count in the admin bar goes up.

Fifty milliseconds is a reasonable default and it is wrong for most sites.

On a tuned server a 20 ms query repeated forty times is a far worse problem than one 60 ms query, and only the second one turns red.

Set it to something that matches your site before you trust the colours: `define( 'QM_DB_EXPENSIVE', 0.01 );` flags anything over 10 ms.

## The method, in the order that finds things fastest

### Work the panels in this order

Load the slowest real page as a logged-out user in one browser and as an administrator in another, because they are different requests with different costs.

Then work the panels in this order. Each step either finds the culprit or eliminates a whole category.

- **Read the admin bar first.** Four numbers. If total query time is a small fraction of page generation time, stop looking at the database entirely. This single comparison saves most people an afternoon.- **Queries by Component.** Sort by time, not by count. This is the panel in the screenshot above, and it names plugins directly. A component with 4 queries and 400 ms outranks one with 200 queries and 30 ms.- **Queries by Caller.** When a component looks bad, this tells you which function inside it. That is the difference between "disable this plugin" and "disable this one feature of this plugin".- **HTTP API Calls.** The panel that finds the real killers. Every server-side request the site makes, with the time taken and the responsible component. A licence check or an analytics ping that runs synchronously on page load costs whole seconds and produces zero database queries, so it is invisible everywhere else.- **Timeline.** Where the time went across the request, rather than which subsystem it was in. Use it when the first four panels all look fine and the page is still slow.- **Scripts and Styles.** Every handle, its URL, its dependencies, and any broken or missing dependency. This is where you find out that four plugins each enqueue their own copy of the same library.- **Hooks and Actions.** Last, because it is the noisiest. Useful when a plugin is doing work on a hook that fires far more often than its author assumed.
The order is deliberate.

Steps 1 and 4 catch the two things that produce the most wasted debugging: a slow page that has nothing to do with the database, and a slow page caused by a network call to somebody else's server.

![Bar chart contrasting 1.82 seconds of page generation time with 0.0052 seconds of database query time.](https://adityaarsharma.com/wp-content/uploads/2026/09/33c7e48b-0f51-44f5-ab85-1d2da9d0fc98_2912x1632-scaled.png)Built from the admin bar readout at the top of this post: 1.82s, 28,840KB, 0.0052s, 19Q.

## What the numbers actually mean

| Number | What it is | How to read it |
| ------ | ---------- | -------------- |
| Page generation time | Wall-clock time PHP spent building the response | The budget everything else is spent from. Compare every other figure to this one. |
| Peak memory usage | Highest memory PHP held during the request | Close to your `memory_limit` means you are one plugin away from a white screen. |
| Database query time | Total time inside the database driver | If this is under 10 percent of generation time, the database is not your problem. |
| Query count | Number of queries run | The most over-quoted number in WordPress. A high count with low time is fine. |
| Red query rows | Queries slower than `QM_DB_EXPENSIVE` | Default 0.05 s. Change the constant or the colours mislead you. |
| Duplicate query warnings | The identical query run more than once | The strongest signal of a missing object cache, because a persistent one removes exactly this. |

### Duplicate queries, and which cache owns them

That last row is where this connects to caching. Duplicate queries in a single request are what a non-persistent object cache already prevents.

Duplicate queries *across* requests are invisible to Query Monitor and are what a persistent object cache fixes. If your Queries by Component panel is clean and the site is still slow under traffic, that is the gap.

Newsletter

## Agents in Production

I check the things our industry takes on trust and publish what I actually found, including when it makes my own work look worse. One researched piece a week.

Email address

Get it weekly

Free. One email a week. Unsubscribe in one click, and I do not send anything else.

## Five things Query Monitor cannot see

This is the part that makes it useful rather than magic. Knowing the blind spots stops you concluding a site is healthy when the measurement simply did not reach.

- **Anything outside PHP.** Network time to your server, TLS negotiation, CDN behaviour, the opcode cache. On my own site the largest single cost is a round trip to origin that happens before PHP starts, and Query Monitor is structurally incapable of showing it.- **The front end as a real visitor sees it.** Query Monitor renders for logged-in users with permission. An administrator request loads the admin bar, more capability checks and often more plugin code than an anonymous visitor ever triggers, so the numbers are an upper bound.- **Anything served from the page cache.** If your page cache is working, the fast path never reaches PHP, so there is nothing to profile. You are always profiling a cache miss.- **Browser-side cost.** Script execution, layout, interaction latency. Query Monitor lists the scripts a page enqueues, and says nothing about what they do once they run.- **Cron.** Work deferred to `wp-cron` disappears from the request that scheduled it and reappears in some unlucky visitor's page load later.

### One honest note about this post

I did not screenshot Query Monitor running on my own site.

`wp-admin` on adityaarsharma.com is behind a custom login path I did not use for this write-up, and I am not going to fabricate a panel or pass off a stock image as my own.

The screenshot above is the real plugin directory page, and the panel in its header is the plugin author's own screenshot, which is what it is.

![The querymonitor.com documentation site, the plugin](https://adityaarsharma.com/wp-content/uploads/2026/09/0cbd1cbd-6fa5-4a6c-8eb8-5c3d2f80139d_2800x2000-scaled.png)querymonitor.com, screenshot taken 3 September 2026.

## The failure mode of the deactivate-everything method

It is worth saying why the common advice fails rather than only saying it does.

### Plugins are slow in combination, not alone

Deactivating plugins one at a time tests plugins in isolation, and slow WordPress sites are usually slow because of interaction. Plugin A adds a meta query. Plugin B adds a filter that runs on every post in the loop.

Neither is slow alone. Together they are quadratic. Turn either one off and the site is fast, which tells you both are innocent and both are guilty, and you will conclude the last one you toggled is the culprit.

It also cannot see cost that is not attributable to activation at all: a plugin that is fine until the options table has 400 autoloaded rows, or one whose licence check is fast in your country and slow from your host's datacentre.

Query Monitor's Queries by Caller and HTTP API Calls panels find both of those in one page load, on the site as it actually is, without taking anything down.

## Do this in the next ten minutes

Install Query Monitor on your staging copy, add `define( 'QM_DB_EXPENSIVE', 0.01 );` to `wp-config.php`, and confirm the drop-in exists:

`ls -la wp-content/db.php`
Then load your slowest template and read exactly two things: the four numbers in the admin bar, and the HTTP API Calls panel.

If database query time is a small share of page generation time and there is a multi-hundred-millisecond outbound request in that second panel, you have found your plugin, and you never deactivated anything.

## Related reading

- What the profiler cannot see, measured from outside with curl on this same site: [what actually makes a WordPress site slow](https://adityaarsharma.com/what-actually-makes-a-wordpress-site-slow/).- Duplicate queries across requests are an object cache problem, and this is which layer owns which symptom: [WordPress caching layers, and which one your problem is actually in](https://adityaarsharma.com/wordpress-caching-layers-which-one-is-failing/).- How the finished page is graded once PHP has done its part: [Core Web Vitals for WordPress in 2026](https://adityaarsharma.com/core-web-vitals-wordpress-2026/).- The Scripts and Styles panel exists because plugins enqueue carelessly, and this is what that looks like from the theme side: [how to fix media query not working in WordPress](https://adityaarsharma.com/how-to-fix-media-query-not-working-in-wordpress/).- Knowing which plugins you are actually paying for and running, before you profile them: [managing WordPress plugin subscriptions for clients](https://adityaarsharma.com/managing-wordpress-plugin-subscriptions-for-clients/).- A small piece of the same plugin-internals surface, written from the other direction: [add a Go to Settings link after installing any WordPress plugin](https://adityaarsharma.com/go-to-plugin-settings-link-after-install/).

## Resources

- [Query Monitor 4.0.7](https://wordpress.org/plugins/query-monitor/) on the WordPress.org plugin directory, read 2 September 2026.- [johnbillion/query-monitor](https://github.com/johnbillion/query-monitor) on GitHub. The db.php explanation and both constants above are quoted from this repository.- [querymonitor.com](https://querymonitor.com/), the plugin's own documentation site.- [Creating the db.php symlink manually](https://querymonitor.com/help/db-php-symlink/), for hosts with a read-only wp-content.- [WP_DEBUG and related constants](https://developer.wordpress.org/apis/wp-config-php/#debug), WordPress developer reference.- [wpdb](https://developer.wordpress.org/reference/classes/wpdb/), the class Query Monitor subclasses to do all of this.