
How to Debug WordPress with WP_DEBUG and Query Monitor?
A WordPress page suddenly loads in eight seconds instead of one. A contact form stops sending, a widget disappears, or a client reports "a blank page" and nothing else. The default WordPress setup hides almost everything that would explain these problems: PHP notices are suppressed, database queries are invisible, and failed calls to external APIs leave no trace. Most debugging sessions that drag on for hours are really sessions spent guessing.
WordPress ships with a set of debugging constants that make errors visible, and the free Query Monitor plugin adds a developer panel that shows every database query, hook, HTTP request, and PHP error behind a page. Used together, they turn guessing into reading.
This article covers the WordPress debugging constants and the safest way to configure them, how to read and rotate the debug log, recovery mode and Site Health, how to install and read Query Monitor's most useful panels, how to log your own values and timings, and a repeatable workflow for tracking down slow pages and broken features without exposing errors to visitors.
The WordPress Debugging Constants
All of WordPress's built-in debugging is controlled from wp-config.php. Each constant does one job:
| Constant | What it does | Typical value in development |
|---|---|---|
WP_DEBUG | Master switch. Reports all PHP errors, notices, and deprecations | true |
WP_DEBUG_LOG | Writes errors to a log file instead of (or as well as) the screen | true or a file path |
WP_DEBUG_DISPLAY | Prints errors into the page HTML | false |
SCRIPT_DEBUG | Loads unminified core JavaScript and CSS | true when debugging JS |
SAVEQUERIES | Stores every database query with its time and caller | true only while profiling |
WP_ENVIRONMENT_TYPE | Labels the site as local, development, staging, or production | local |
WP_DEBUG_LOG and WP_DEBUG_DISPLAY only take effect when WP_DEBUG is true. With WP_DEBUG off, WordPress reports only serious errors and warnings.
A Safe Development Configuration
Add these lines to wp-config.php above the line that says /* That's all, stop editing! Happy publishing. */. Constants defined after wp-settings.php is loaded are ignored.
// wp-config.php
define( 'WP_ENVIRONMENT_TYPE', 'local' );
define( 'WP_DEBUG', true );
define( 'WP_DEBUG_LOG', true );
define( 'WP_DEBUG_DISPLAY', false );
define( 'SCRIPT_DEBUG', true );
This setup reports every error but writes them to wp-content/debug.log instead of the page. Errors printed into HTML break layouts, corrupt JSON responses from the REST API and admin-ajax, and can reveal file paths to anyone viewing the page. Logging to a file gives you the same information without those side effects.
If you prefer to see errors on screen while working locally, set WP_DEBUG_DISPLAY to true, but never do that on a site the public can reach.
Moving the Log Out of the Web Root
By default, debug.log lives in wp-content/, which is usually publicly accessible. Anyone who guesses the URL can read your error messages, including file paths, plugin names, and sometimes data. Since WordPress 5.1, WP_DEBUG_LOG accepts a file path, so you can write the log somewhere the web server does not serve:
// wp-config.php
define( 'WP_DEBUG', true );
define( 'WP_DEBUG_LOG', '/home/example/logs/wp-debug.log' );
define( 'WP_DEBUG_DISPLAY', false );
The directory must exist and be writable by the PHP user. If you cannot write outside the web root, block access to the default file in your server configuration. For Nginx:
# /etc/nginx/sites-available/example.com
location = /wp-content/debug.log {
deny all;
}
For Apache, in the .htaccess file inside wp-content:
# wp-content/.htaccess
<Files "debug.log">
Require all denied
</Files>
Toggling Constants with WP-CLI
On servers where you have shell access, WP-CLI edits wp-config.php without opening an editor, which is less error-prone than editing by hand:
# Terminal
wp config set WP_DEBUG true --raw
wp config set WP_DEBUG_LOG true --raw
wp config set WP_DEBUG_DISPLAY false --raw
# Turn debugging off again when you are done
wp config set WP_DEBUG false --raw
The --raw flag writes true as a PHP boolean rather than the string 'true'. More command-line techniques are covered in how to use WP-CLI to manage WordPress.
Reading the Debug Log
Once logging is on, reproduce the problem, then read the end of the log:
# Terminal
tail -n 50 wp-content/debug.log
# Or watch it live while you click around the site
tail -f wp-content/debug.log
A typical entry looks like this:
[09-Oct-2026 08:14:52 UTC] PHP Warning: Undefined array key "price" in /var/www/html/wp-content/plugins/acme-shop/includes/cart.php on line 212
[09-Oct-2026 08:14:53 UTC] PHP Fatal error: Uncaught Error: Call to undefined function acme_get_rates() in /var/www/html/wp-content/themes/acme/functions.php:48
Read each entry for three things: the severity (fatal errors stop execution, warnings and notices usually do not), the file path, which tells you which plugin or theme is responsible, and the line number. A file path under wp-content/plugins/acme-shop/ points at that plugin, regardless of where the symptom appears.
| Severity | What it means | Urgency |
|---|---|---|
Fatal error | Execution stopped. Usually the cause of a blank page | Fix now |
Warning | Something went wrong but the code continued | Fix soon |
Notice / Deprecated | Sloppy or outdated code that still works today | Fix during updates |
Deprecation notices deserve attention before major PHP or WordPress upgrades, because today's deprecation is tomorrow's fatal error.
Logging Your Own Values
To check what a variable contains at a certain point, write it to the same log with PHP's error_log():
// wp-content/themes/acme/functions.php
add_action( 'pre_get_posts', function ( WP_Query $query ) {
if ( $query->is_main_query() && ! is_admin() ) {
error_log( 'Main query vars: ' . wp_json_encode( $query->query_vars ) );
}
} );
wp_json_encode() or print_r( $value, true ) turns arrays and objects into readable strings. Remove these lines when you are done; logs grow quickly on busy sites.
Keeping the Log Under Control
A debug log left running on a busy site can grow to gigabytes. Delete or truncate it after each session, and turn WP_DEBUG off when you finish. On servers where you keep logging on permanently for errors, set up logrotate for the file rather than letting it grow forever.
Recovery Mode and Site Health
Two core features help before you even open a log.
Recovery mode, added in WordPress 5.2, catches fatal errors caused by a plugin or theme. Instead of a blank page, visitors see "There has been a critical error on this website," and WordPress emails the site administrator a special login link. That link opens the dashboard with the faulty plugin or theme paused, so you can deactivate or fix it. If the admin email address is unmonitored, send these emails somewhere useful:
// wp-config.php
define( 'RECOVERY_MODE_EMAIL', 'developer@example.com' );
If WordPress emails are unreliable on your server, recovery links may never arrive. Fixing email delivery, as explained in how to send reliable WordPress emails with SMTP, makes recovery mode far more useful.
Site Health, under Tools → Site Health, runs checks for outdated PHP, failing loopback requests, missing PHP extensions, and scheduled event problems. The Info tab lists the server configuration, active plugins, constants, and file permissions, and has a Copy site info to clipboard button that is ideal for support requests.
For conflicts that only appear with certain plugins active, the free Health Check & Troubleshooting plugin adds a troubleshooting mode that disables plugins and switches to a default theme for your session only. Visitors keep seeing the normal site while you re-enable plugins one at a time to find the culprit.
Installing Query Monitor
Query Monitor is a free plugin by John Blackbourn, available from the WordPress.org plugin directory:
- Go to Plugins → Add New Plugin.
- Search for Query Monitor.
- Click Install Now, then Activate.
With WP-CLI:
# Terminal
wp plugin install query-monitor --activate
After activation, a new item appears in the admin toolbar showing the page generation time, peak memory use, database query time, and query count. Click it to open the panel. By default, only administrators see Query Monitor output, so it is reasonably safe even on a staging or production site, though it does add overhead to every request for logged-in admins.
On activation, Query Monitor tries to create a symlink at wp-content/db.php to its own database drop-in. This drop-in lets it capture extra details such as the full call stack of each query. If the file cannot be created, or another plugin already uses db.php, Query Monitor still works with slightly less detail.
The Query Monitor Panels That Matter Most
Query Monitor has many panels. These are the ones that solve most real problems.
Overview
The overview shows total page generation time, memory, and database totals. Compare a slow page with a fast one. If database time accounts for most of the total, look at queries. If it is small but the page is still slow, look at HTTP API calls or PHP-heavy code.
Queries, Queries by Caller, and Queries by Component
The Queries panel lists every SQL query with its execution time, the function that ran it, and the component responsible. Slow queries, usually anything over 0.05 seconds, are highlighted.
The Queries by Component view is the fastest way to find a problem plugin. It totals the queries and time per plugin, theme, and core. A plugin running 300 queries on a page that needs 40 is your lead.
Duplicate Queries shows identical queries run more than once on the same page, a common symptom of code that fetches data inside a loop instead of once. Caching those results, or adding a persistent object cache, often removes the duplicates entirely. The object cache side is covered in mastering object cache in WordPress.
PHP Errors
This panel lists warnings, notices, and deprecations from the current request, with the component and call stack. It works even when WP_DEBUG_DISPLAY is off, so you see errors without printing them into the page. The toolbar item turns red or orange when errors are present.
Hooks & Actions
This panel lists every action and filter fired during the request, in order, with every callback attached and its priority. Use it to answer questions such as "Is my function hooked at all?" or "Which plugin removes this filter?" You can filter the list by component.
HTTP API Calls
Every outbound request made through the WordPress HTTP API appears here, including license checks, update checks, and calls to payment, email, or social APIs. Each entry shows the URL, response code, and time taken. A single call to an unresponsive remote server can add several seconds to a page, and this panel is often the only place that shows it.
Template, Conditionals, and Request
- Template shows which template file rendered the page, the full template hierarchy WordPress considered, and, for block themes, the block template used. It answers "Why is my template change not showing?"
- Conditionals lists which conditional functions are true, such as
is_single()oris_front_page(), which helps when code runs on the wrong pages. - Request shows the matched rewrite rule and query variables, useful for debugging custom permalinks and custom post types.
Scripts and Styles
These list every enqueued script and stylesheet with its handle, dependencies, version, and the component that added it. They also flag broken dependencies and assets that failed to load.
Debugging AJAX and REST Requests
Query Monitor also collects data for admin-ajax and REST API requests made by the page. For AJAX requests, PHP errors are added as response headers and summarized in the browser console. For REST requests, add _envelope to the request URL to see Query Monitor's data inside the response. This is handy when a block editor feature fails silently.
Logging and Profiling with Query Monitor
Instead of writing to debug.log, you can send values directly to Query Monitor's Logs panel with actions. If Query Monitor is deactivated, these calls do nothing, so they cannot cause a fatal error:
// wp-content/plugins/acme-shop/includes/cart.php
do_action( 'qm/debug', $cart_items );
do_action( 'qm/info', 'Applied coupon {code}', array( 'code' => $coupon_code ) );
do_action( 'qm/warning', 'Shipping rates missing for zone ' . $zone_id );
do_action( 'qm/error', $wp_error );
Query Monitor supports the PSR-3 log levels: emergency, alert, critical, error, warning, notice, info, and debug. Placeholders in braces are replaced from the context array.
To time a piece of code, use the timer actions. The results appear in the Timings panel with elapsed time and memory:
// wp-content/plugins/acme-shop/includes/rates.php
do_action( 'qm/start', 'acme_shipping_rates' );
$rates = acme_calculate_rates( $cart );
do_action( 'qm/stop', 'acme_shipping_rates' );
For loops, qm/lap records intermediate times:
// wp-content/plugins/acme-shop/includes/import.php
do_action( 'qm/start', 'product_import' );
foreach ( $rows as $row ) {
acme_import_product( $row );
do_action( 'qm/lap', 'product_import' );
}
do_action( 'qm/stop', 'product_import' );
SAVEQUERIES Without Query Monitor
SAVEQUERIES makes $wpdb keep every query with its execution time and caller in $wpdb->queries. Query Monitor captures queries on its own, so you only need SAVEQUERIES when writing a custom profiling script or when you cannot install plugins. A small mu-plugin can write the slowest queries to the log:
// wp-content/mu-plugins/log-slow-queries.php
<?php
/**
* Plugin Name: Log Slow Queries
* Description: Logs queries slower than 50 ms. Requires SAVEQUERIES. Development only.
*/
add_action( 'shutdown', function () {
global $wpdb;
if ( ! defined( 'SAVEQUERIES' ) || ! SAVEQUERIES || empty( $wpdb->queries ) ) {
return;
}
foreach ( $wpdb->queries as $query ) {
list( $sql, $time, $caller ) = $query;
if ( $time > 0.05 ) {
error_log( sprintf( '[slow query %.4fs] %s | %s', $time, $sql, $caller ) );
}
}
} );
SAVEQUERIES stores every query in memory and slows every request, so enable it only while you are actively profiling.
Using WP_ENVIRONMENT_TYPE to Keep Debug Code Out of Production
WP_ENVIRONMENT_TYPE lets code behave differently per environment. Read it with wp_get_environment_type(), which returns production when nothing is set:
// wp-content/mu-plugins/environment-tweaks.php
<?php
/**
* Plugin Name: Environment Tweaks
*/
if ( 'production' !== wp_get_environment_type() ) {
// Discourage search engines from indexing non-production copies.
add_filter( 'pre_option_blog_public', '__return_zero' );
}
Setting the environment type correctly on local and staging copies, as recommended in how to set up a WordPress staging site, also lets plugins such as Query Monitor and Jetpack adjust their behavior automatically.
A Repeatable Debugging Workflow
When something breaks, work through the same steps every time:
- Reproduce the problem and note exactly which URL, user role, and action triggers it.
- Check the log. Enable
WP_DEBUGandWP_DEBUG_LOGwith display off, reproduce, and read the newest entries. - Open Query Monitor on the affected page. Check PHP Errors, then the overview for where the time goes.
- Find the component. File paths in errors, Queries by Component, and HTTP API Calls usually point to one plugin or the theme.
- Confirm with isolation. Use troubleshooting mode or a staging copy to disable the suspect and confirm the problem disappears.
- Fix, update, or replace the component, then re-test with Query Monitor to confirm the numbers improved.
- Turn debugging off and delete the log.
For a broader list of specific error messages and their fixes, see how to troubleshoot common WordPress errors.
Common Problems and Fixes
- No debug.log file appears.
WP_DEBUGis stillfalse, the constants were added below the "stop editing" line, or the PHP user cannot write towp-content. Move the lines up and check permissions. - Errors still show on the page with
WP_DEBUG_DISPLAYset tofalse. Another file, often a hosting control panel setting orphp.ini, forcesdisplay_errorson. Check the PHP configuration in Site Health's Info tab. - Query Monitor does not appear in the toolbar. The toolbar is hidden in your profile settings, you are not an administrator, or the page is cached for logged-in users. Disable page caching for logged-in sessions.
- Query Monitor shows no caller details for queries. The
db.phpdrop-in was not created, often because another plugin already uses it. Query Monitor still works with less detail. - The log fills with deprecation notices from a plugin you cannot change. Report them to the plugin author and update when a fix ships. Do not lower error reporting globally, or you will miss real problems.
- Recovery mode email never arrives. Set
RECOVERY_MODE_EMAILand fix outgoing mail with an SMTP service.
WordPress Debugging FAQ
Only with display turned off and the log written outside the public web root. Showing errors to visitors exposes file paths and breaks pages, and a log in a public folder can be read by anyone who guesses its URL. Most sites should keep WP_DEBUG off in production and turn it on briefly when investigating.
By default it is wp-content/debug.log. Since WordPress 5.1 you can set WP_DEBUG_LOG to a full file path to store it elsewhere, ideally in a directory the web server does not serve.
It adds overhead only for users who can view its output, which by default means administrators. Visitors are not affected. Deactivate it when you are not using it, especially on production sites.
Yes. In Query Monitor's Settings panel you can set an authentication cookie that keeps the output visible after you log out, so you can debug what visitors see in the same browser.
WP_DEBUG controls PHP error reporting. SCRIPT_DEBUG makes WordPress load the unminified versions of its core JavaScript and CSS files, which makes browser console errors and stack traces readable. They are independent and can be used together.
Conclusion
Most WordPress problems are easy to fix once you can see them. WP_DEBUG with logging on and display off records every PHP error without breaking pages. A custom log path keeps that information private. Recovery mode and Site Health catch fatal errors and configuration problems before you open a single file. Query Monitor then shows what the page is really doing: which queries are slow or duplicated, which plugin is responsible, which hooks fire, and which external calls are holding everything up.
Build the habit of working through the same steps every time, confirm suspects on a staging copy, and switch debugging off when you are finished.
Here are some useful references for going deeper on WordPress debugging:
- WordPress Developer Docs: Debugging in WordPress — every debugging constant and how to use it.
- WordPress Developer Docs: wp_get_environment_type() — environment types and their default.
- Query Monitor: Official documentation — panels, logging, profiling, and REST and AJAX debugging.
- WordPress.org Plugins: Health Check & Troubleshooting — per-session troubleshooting mode for plugin conflicts.
- PHP Manual: error_log() — how PHP writes messages to the error log.


