Type something to search...
How to Debug WordPress with WP_DEBUG and Query Monitor?

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:

ConstantWhat it doesTypical value in development
WP_DEBUGMaster switch. Reports all PHP errors, notices, and deprecationstrue
WP_DEBUG_LOGWrites errors to a log file instead of (or as well as) the screentrue or a file path
WP_DEBUG_DISPLAYPrints errors into the page HTMLfalse
SCRIPT_DEBUGLoads unminified core JavaScript and CSStrue when debugging JS
SAVEQUERIESStores every database query with its time and callertrue only while profiling
WP_ENVIRONMENT_TYPELabels the site as local, development, staging, or productionlocal

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.

SeverityWhat it meansUrgency
Fatal errorExecution stopped. Usually the cause of a blank pageFix now
WarningSomething went wrong but the code continuedFix soon
Notice / DeprecatedSloppy or outdated code that still works todayFix 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:

  1. Go to Plugins → Add New Plugin.
  2. Search for Query Monitor.
  3. 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() or is_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:

  1. Reproduce the problem and note exactly which URL, user role, and action triggers it.
  2. Check the log. Enable WP_DEBUG and WP_DEBUG_LOG with display off, reproduce, and read the newest entries.
  3. Open Query Monitor on the affected page. Check PHP Errors, then the overview for where the time goes.
  4. Find the component. File paths in errors, Queries by Component, and HTTP API Calls usually point to one plugin or the theme.
  5. Confirm with isolation. Use troubleshooting mode or a staging copy to disable the suspect and confirm the problem disappears.
  6. Fix, update, or replace the component, then re-test with Query Monitor to confirm the numbers improved.
  7. 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_DEBUG is still false, the constants were added below the "stop editing" line, or the PHP user cannot write to wp-content. Move the lines up and check permissions.
  • Errors still show on the page with WP_DEBUG_DISPLAY set to false. Another file, often a hosting control panel setting or php.ini, forces display_errors on. 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.php drop-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_EMAIL and 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:

  1. WordPress Developer Docs: Debugging in WordPress — every debugging constant and how to use it.
  2. WordPress Developer Docs: wp_get_environment_type() — environment types and their default.
  3. Query Monitor: Official documentation — panels, logging, profiling, and REST and AJAX debugging.
  4. WordPress.org Plugins: Health Check & Troubleshooting — per-session troubleshooting mode for plugin conflicts.
  5. PHP Manual: error_log() — how PHP writes messages to the error log.
Tags :
Share :

Related Posts

WordPress optimization with specific recommended approach

WordPress optimization with specific recommended approach

Whether you run a high traffic WordPress installation or a small blog on a low cost shared host, you should optimize WordPress and your server to run

Continue Reading
Creating and Customizing WordPress Child Themes

Creating and Customizing WordPress Child Themes

Creating a child theme in WordPress is a best practice for making modifications to a theme. By using a child theme, you can update the parent theme w

Continue Reading
Understanding the Distinction Categories vs. Tags in WordPress

Understanding the Distinction Categories vs. Tags in WordPress

WordPress, a powerful content management system, offers a plethora of features to organize content effectively. Among these features, categories and

Continue Reading