NewTry MagicWP now - first month free
Back to blog

Profiling WordPress with Query Monitor: Finding the Actual Bottleneck

Stop guessing which plugin is slow. Query Monitor attributes every query, hook and HTTP call to the code responsible. Here's how to read it.

Profiling WordPress with Query Monitor: Finding the Actual Bottleneck

The standard way people find a slow plugin is to deactivate plugins one at a time until the site speeds up. It works, eventually, and it costs an afternoon and requires taking things offline.

There's a better way, and it's free. Query Monitor sits in your admin bar and tells you, for the page you're currently looking at, every database query that ran, how long each took, and which plugin was responsible. It also shows the HTTP requests your server made, the hooks that fired, the scripts and styles enqueued, and the PHP errors nobody surfaced.

This walks through installing it properly, reading the panels that matter, and turning what you find into a fix.

TL;DR

  • Query Monitor is free, has 200,000+ active installs, and is maintained by John Blackbourn. It requires PHP 7.4 or later.
  • The db.php symlink is the part most people skip. Without it you still get query data, but you lose result counts, full stack traces, and error detection. It's usually created automatically on activation.
  • Its most valuable feature is component attribution: it parses the call stack for each query and maps it to the responsible plugin, theme, or core.
  • It does not require the SAVEQUERIES constant. That's a common misconception; Query Monitor's drop-in captures query data independently.
  • By default output is only visible to logged-in administrators. There's a built-in authentication cookie setting for viewing output while logged out, which matters for profiling cached front-end requests.
  • Query Monitor flags slow and duplicate queries but doesn't publish a fixed millisecond threshold for "slow," so treat its highlighting as directional.

Prerequisites

Before starting:

  • Administrator access to the WordPress site.
  • A staging copy rather than production. Query Monitor adds overhead, and the fixes you find will need testing anyway. See MagicWP's site tools documentation.
  • A backup, since you'll potentially be deactivating or replacing plugins based on what you find. MagicWP's on-demand backups cover this.
  • Awareness of any existing db.php drop-in. If you run W3 Total Cache, LudicrousDB, HyperDB, or the SQLite Database Integration plugin, they occupy the same slot Query Monitor wants. See Step 2.
  • SSH access is helpful but not required. MagicWP provides SSH with WP-CLI on every site; see the SFTP and SSH docs.

Useful background: WordPress's official Debugging in WordPress documentation covers WP_DEBUG, WP_DEBUG_LOG, and SAVEQUERIES, all of which complement what Query Monitor does.

Step 1 - Install Query Monitor

Go to Plugins → Add New, search for "Query Monitor," install and activate it. The author is John Blackbourn; check that before installing, since several plugins have similar names.

After activation you'll see a new item in your admin toolbar showing a set of numbers. That's your at-a-glance summary: page generation time, peak memory usage, database query time, and query count.

Those four numbers alone are useful. If page generation is 2.4 seconds and database time is 2.1 of it, you have a query problem. If page generation is 2.4 seconds and database time is 80 milliseconds, you have a PHP problem, and the queries aren't where to look.

Step 2 - Set Up the db.php Drop-In

This is the step people skip, and it's the difference between decent data and good data.

Query Monitor ships a db.php drop-in that extends WordPress's wpdb class. WordPress loads wp-content/db.php before plugins are loaded, which means the drop-in can capture every query from the earliest possible point in the request rather than only those issued after Query Monitor itself initialises.

What it adds, per Query Monitor's own documentation: result count, full stack trace, and error detection for all database queries. Without it, Query Monitor still works, but loses that extended information.

Query Monitor usually creates the symlink automatically on activation. Check whether it worked by looking at the Database Queries panel. If you see full stack traces for each query, you're set.

If it didn't, you have three options:

Over WP-CLI:

terminal
wp qm enable

Manually, from the site root:

terminal
ln -s wp-content/plugins/query-monitor/wp-content/db.php wp-content/db.php

Or through your host's file manager, copying the file rather than symlinking it.

Important: WordPress loads only one wp-content/db.php. If a caching or database plugin already occupies that slot, Query Monitor's drop-in cannot coexist with it. Known conflicts include W3 Total Cache, LudicrousDB, HyperDB, and the SQLite Database Integration plugin. On a staging copy, temporarily deactivating the conflicting plugin to profile is usually acceptable. On production, it isn't.

Step 3 - Read the Database Queries Panel

This is where most WordPress performance problems live.

Open the Query Monitor menu in your admin bar and select Queries. You'll see every database query the current page ran.

Sort by time. The slowest query is at the top, and on most sites a small number of queries account for most of the database time.

Look at the Component column. This is Query Monitor's most valuable feature. For each query it captures the full PHP call stack, parses it to identify the responsible file and function, and maps that back to a component: core, a specific plugin, a specific theme, or Query Monitor itself.

This is how "the site is slow" becomes "this plugin runs a 400 millisecond query on every page load."

Look at the Caller column for the specific function that issued the query. When a plugin runs several slow queries, the caller tells you which of its features is responsible, which matters when the plugin has settings you can change.

Check the sub-tabs. Query Monitor groups queries by type (SELECT, UPDATE, and so on), by calling function, and by component, so you can jump straight to "show me everything this plugin did."

Watch for duplicates. Query Monitor flags queries that ran more than once with identical SQL. Duplicates usually mean an N+1 pattern: code looping over a set of items and querying inside the loop instead of fetching them together. It's one of the most common and most fixable performance bugs.

A note on thresholds: Query Monitor highlights slow and duplicate queries, but its documentation doesn't publish a specific millisecond cutoff for what counts as slow. Treat the highlighting as directional rather than authoritative, and use your own judgement about what's acceptable for the page in question.

Step 4 - Check the HTTP API Calls Panel

This panel finds a class of problem the queries panel can't.

Select HTTP API Calls. It lists every outbound HTTP request your server made while generating this page, with response codes and timing.

Every one of these is your server waiting on a third-party network. A plugin checking a licence server, fetching social counts, calling an analytics API, or validating something remotely blocks page generation for as long as that call takes.

What to look for:

Any call taking more than a couple hundred milliseconds. That time is added directly to your page generation.

Calls that shouldn't be happening on a front-end page load. Licence checks belong on admin screens, not on every visitor request.

Failed or timed-out requests. A call to an unreachable endpoint can hang until the timeout expires, which is usually five seconds. A visitor waiting five seconds for a licence check to fail is a genuinely bad outcome, and it's invisible in every front-end speed test that doesn't happen to run during the outage.

Step 5 - Use the Timings and Other Panels

Query Monitor has more panels than most people ever open. The ones worth knowing:

Timings profiles specific segments of the request, letting you see where PHP time went beyond the database.

Scripts and Styles lists everything enqueued with its dependency tree. Useful for finding a plugin loading its entire front-end bundle on pages where it isn't used.

PHP Errors surfaces errors, warnings, and notices with the responsible component and call stack, including ones your error display settings would hide. Suppressed warnings firing on every request cost real time.

Hooks & Actions shows what fired and what was attached, which helps when a plugin is hooking something expensive into a frequently-fired action.

Transients lists transients set or updated during the request, which is useful when tracking down wp_options bloat.

Conditionals shows which WordPress conditional functions returned true, useful for confirming which template is actually rendering.

Capability Checks shows permission checks with their results and arguments.

Environment reports PHP, database, WordPress, and web server details in one place.

Step 6 - Profile Logged-Out Requests

Here's a limitation that catches people. By default, Query Monitor's output is only visible while logged in as an administrator. That means you're profiling the admin experience, which isn't what your visitors get.

The difference matters. Logged-in requests bypass page caching, load the admin bar, and may trigger different code paths. A query that only runs for logged-in users will show up in your profiling and never affect a single visitor.

Query Monitor provides a "Set authentication cookie" feature in its settings, which lets you view its output while not logged in as that user. This is how you profile the actual front-end experience.

Two practical notes. Some hosting environments have known issues with this cookie approach, so verify it works on yours before relying on it. And remember that if page caching is active, a cached response won't run PHP at all, so you'll need to bypass the cache to profile the real generation path.

Step 7 - Turn Findings Into Fixes

Profiling produces a list. Here's what to do with each kind of finding.

A slow query attributed to a plugin. Check the plugin's settings first, since the expensive behaviour is often a feature you can disable. Then check for an update. Then consider whether you need the plugin. Reporting it to the developer with the specific query and timing from Query Monitor is genuinely useful to them and costs you five minutes.

Duplicate queries in a loop. If it's your code, fetch the data in one query before the loop. If it's a plugin's, report it with the evidence.

A slow HTTP call on the front end. Check whether the plugin can be configured to do it less often or only in admin. Many licence-check implementations have a setting for this.

Enormous numbers of queries — several hundred on a simple page — usually means a plugin querying inside a loop or an object cache that isn't working. Check whether a persistent object cache is active before assuming it's a plugin.

High page generation time with low database time. Your problem is PHP execution, not queries. Look at the Timings panel, and consider whether OPcache is enabled and adequately sized.

Complementary Tools

Query Monitor tells you about a single request in detail. Two other tools cover what it doesn't.

SAVEQUERIES is a WordPress core constant that, per the official documentation, "saves database queries to an array, which can then be displayed to help analyze those queries," including the query, its execution time, and the calling function, available via $wpdb->queries.

Two things worth knowing. WordPress's documentation attaches a clear warning: "This will have a performance impact on your site, so make sure to turn it off when you aren't debugging." And Query Monitor does not require it — its db.php drop-in captures query data independently, and gives you more. If you've enabled SAVEQUERIES specifically for Query Monitor, you can turn it off.

The Performance Lab plugin, maintained by the WordPress performance team, ships a Server-Timing API that emits timing metrics as HTTP response headers. Its default metrics are wp-before-template, wp-template, and wp-total, splitting the request into WordPress bootstrap, template rendering, and total.

This is complementary rather than overlapping. Query Monitor gives depth on one request; Server-Timing gives you a number you can collect across many requests, including from real visitors.

The WordPress performance team also publishes a benchmarking methodology worth following: test on a single site to isolate variables, run dozens to hundreds of requests rather than manual spot checks, and compare median (p50) values across scenarios rather than single-run numbers. That last point matters. A single before-and-after comparison tells you almost nothing given normal request-to-request variance.

Frequently Asked Questions

Is Query Monitor safe to run on a production site? It adds overhead to every request it profiles, so it's better on staging. If you must use it on production, activate it briefly, gather what you need, and deactivate it. Its output is only visible to administrators by default.

Does Query Monitor need SAVEQUERIES enabled? No. Its db.php drop-in captures query data independently and provides more detail than SAVEQUERIES does, including full stack traces and result counts. This is a common misconception.

Why don't I see stack traces in the Queries panel? The db.php drop-in probably isn't installed. Query Monitor normally creates the symlink automatically on activation, but it can fail on some hosts or conflict with another plugin's db.php. Run wp qm enable or create the symlink manually.

Can I use Query Monitor with W3 Total Cache? Not simultaneously for full query profiling, since both want the wp-content/db.php slot and WordPress loads only one. On staging you can temporarily deactivate the caching plugin to profile.

How do I profile what logged-out visitors experience? Use Query Monitor's "Set authentication cookie" setting, which lets you view its output while not logged in. Also bypass your page cache, since a cached response doesn't execute PHP at all.

What counts as a slow database query? Query Monitor flags slow queries but doesn't publish a fixed threshold in its documentation. As a working rule, anything over 50 milliseconds on a simple page load is worth investigating, and anything over 200 milliseconds is a problem. Context matters more than a universal number.

How many database queries should a WordPress page make? There's no official target. A simple page on a lean install might run 20 to 40. Several hundred usually indicates a plugin querying inside a loop or a missing object cache. Total query time matters more than count.

Query Monitor shows a slow query but not which plugin caused it. Why? Usually the db.php drop-in isn't active, since attribution relies on the stack trace it captures. Queries originating in core called from a hook can also be harder to attribute; check the Caller column and the Hooks panel.

Conclusion

You now have Query Monitor installed with its drop-in active, and you know which panels answer which question: Queries for database time and plugin attribution, HTTP API Calls for third-party requests blocking page generation, Timings for PHP time that isn't queries, and PHP Errors for problems nobody surfaced.

The habit worth building is checking before you optimise. Most WordPress performance work gets aimed at whatever the last article recommended rather than at what's actually slow on this site. Ten minutes with Query Monitor usually replaces a week of that.

And when you do change something, measure across many requests rather than one. Request-to-request variance is large enough that a single before-and-after comparison can tell you the opposite of the truth.

Next steps

A
Alex
MagicWP
Writing about WordPress, performance, and the infrastructure that makes sites fast.

Get the best of MagicWP in your inbox.

Monthly engineering notes, product updates, and WordPress performance tips. No spam, unsubscribe anytime.

Join 12,000+ builders. We send one email a month.