How to enable and read the WordPress debug log

Another KB article told you to turn on WP_DEBUG and read debug.log. Here is exactly how to do it safely, where the file lives, what each constant really does, and how to keep the log private so visitors never see your errors.

You followed a troubleshooting article. It told you to enable WP_DEBUG, set WP_DEBUG_LOG to true, and read the resulting log to see what is actually breaking. That is good advice. It is also four constants worth of footguns if you do not understand how they interact, and the wrong combination will print every PHP error your site produces directly into the HTML your visitors see. This article walks the safe path: which constants to set, which to explicitly unset, where the log file ends up, how to read what it contains, and how to make sure no one outside your team can read it.

What the WordPress debug log captures

When configured correctly, the WordPress debug log captures every PHP error that happens during a request: parse errors, fatal errors, warnings, notices, and deprecation messages from PHP itself, from WordPress core, from your active theme, and from every active plugin. Each entry is one line written to a plain text file by PHP's error_log() function, with a UTC timestamp, the error level, the error message, and the file path plus line number where it fired.

The mechanism is described in the WordPress debugging handbook and implemented in wp_debug_mode(), which runs early in the WordPress bootstrap and decides what PHP should report and where it should send those reports. When WP_DEBUG is true, that function calls error_reporting( E_ALL ), so PHP starts reporting everything. When WP_DEBUG_LOG is also true, the same function calls ini_set( 'log_errors', 1 ) and ini_set( 'error_log', $log_path ), which routes all those errors to a file you can open and read.

Database errors are a special case: WordPress only prints SQL errors when WP_DEBUG is enabled, regardless of what WP_DEBUG_LOG is set to. So if you are chasing a wpdb problem, the constant you need is WP_DEBUG, not just the log.

Enabling debugging in wp-config.php

The whole configuration lives in four constants in wp-config.php. They have to appear before the /* That's all, stop editing! Happy publishing. */ marker, because the file is loaded top-down and anything below the marker is read after WordPress has already made its decisions about error reporting.

Connect via SFTP, SSH, or your hosting file manager. Open wp-config.php in the WordPress root. Find the line that reads define( 'WP_DEBUG', false ); and replace it with the safe development configuration:

// Turn on WordPress debugging (enables E_ALL reporting)
define( 'WP_DEBUG', true );
// Write all PHP errors to wp-content/debug.log
define( 'WP_DEBUG_LOG', true );
// Hide errors from page output (do NOT skip this line)
define( 'WP_DEBUG_DISPLAY', false );
// Belt-and-braces: also tell PHP directly not to display errors
@ini_set( 'display_errors', 0 );

Save the file and reload the broken page once. You will know the configuration took effect when the file wp-content/debug.log exists after that reload (it is created on first error) and contains entries dated to the moment you reloaded.

The four constants do four different things, and most readers get into trouble by setting one and assuming the others default to something safe. They do not.

  • WP_DEBUG is the master switch. With it off (the default), WordPress restricts PHP's error reporting to fatal-class errors only. Notices and deprecations are suppressed entirely. With it on, PHP reports E_ALL. Without WP_DEBUG = true, the other three constants below are ignored.
  • WP_DEBUG_LOG controls file logging. Set it to true to log to wp-content/debug.log. Set it to a string path (e.g. '/home/user/private-logs/wp-errors.log') to log somewhere else. Setting it to false (the default) means no file logging, even when WP_DEBUG is on.
  • WP_DEBUG_DISPLAY controls whether errors are printed inline in the HTML response. The default is true. Read that again: the default is true. If you set WP_DEBUG = true and forget to also set WP_DEBUG_DISPLAY = false, every PHP warning your site produces is printed directly into the HTML your visitors see.
  • @ini_set( 'display_errors', 0 ) is a redundant safety net. It tells PHP itself not to render errors, in case anything between WordPress's call to wp_debug_mode() and the actual error overrides the display setting. It costs nothing and it has saved more than one site from leaking stack traces.

The string-path form of WP_DEBUG_LOG was added in WordPress 5.1 (January 2019). Before that, you could only log to the default location. If you want a custom path:

define( 'WP_DEBUG', true );
define( 'WP_DEBUG_LOG', '/home/user/private-logs/wp-errors.log' );
define( 'WP_DEBUG_DISPLAY', false );
@ini_set( 'display_errors', 0 );

The directory you point at must already exist and must be writable by the web server user. The file itself is created on first error. Pointing at a non-existent directory silently swallows all logging without telling you.

WP_DEBUG_DISPLAY defaults to TRUE: protect your visitors from leaks

This is the single most important thing in this article. WP_DEBUG_DISPLAY defaults to true. Not false. The official WordPress documentation is explicit on this: "WP_DEBUG_DISPLAY [...] Set this to false to hide errors from being printed. The default is true." That means you have to actively turn it off, not actively turn it on.

The practical consequence: if you set only define( 'WP_DEBUG', true ); and walk away, every visitor to your site will see PHP warnings and notices rendered into the page HTML, sometimes inside the <head>, which breaks the layout, sometimes inside the body, which leaks file paths, plugin names, database table prefixes, and occasionally fragments of database content. Search engines may then crawl and cache that HTML. You do not want any of this.

The fix is the four-line block above. Always set WP_DEBUG_DISPLAY = false and @ini_set( 'display_errors', 0 ) together with WP_DEBUG = true. Treat the two as a single unit. If you only ever copy-paste one snippet from this article, copy that block.

A useful corollary: when you are done debugging, the safest cleanup is not to flip WP_DEBUG back to false and leave the other three lines in place. Remove all four lines, or leave them and switch the configuration to the production-safe form at the bottom of this article.

Finding debug.log

When WP_DEBUG_LOG is true, the log is written to wp-content/debug.log. The exact resolved path is WP_CONTENT_DIR . '/debug.log', evaluated at runtime, so a custom WP_CONTENT_DIR constant moves the file with it. On a default install, that means:

/path/to/your/site/wp-content/debug.log

The file is created on the first error after you enabled logging. An absent file does not mean logging is broken. It means no qualifying error has fired yet. Reload the page that you know is breaking and check again.

If you set WP_DEBUG_LOG to a string path, that path is used instead. Check what is actually in effect from WP-CLI or a one-line PHP eval:

# What does PHP think the active error log is?
wp eval 'echo ini_get("error_log") . PHP_EOL;'

That command prints the absolute path PHP is currently using for error logging, which is the only authoritative answer. Some managed hosts override the error_log ini directive and route all errors to a system log regardless of what WordPress sets, in which case wp-content/debug.log will stay empty even with logging fully enabled. If wp eval shows a path you do not recognise, that is your host's override and you will need to ask the host where to read it.

A few other reasons wp-content/debug.log may be empty even though something is clearly broken:

  • WP_DEBUG is not set to true. Without the master switch, the entire WP_DEBUG_LOG mechanism is skipped.
  • The wp-content directory is not writable by the web server user.
  • The fatal happens during very early bootstrap, before wp-settings.php has loaded. Those errors land in the server-level PHP error log, not the WordPress one.
  • Quoted booleans: define( 'WP_DEBUG', 'false' ); is truthy because 'false' is a non-empty string. Always use raw booleans, never strings.

How to read a PHP error in the log

Each line in debug.log follows the same format that PHP's error_log() function writes when logging to a file:

[08-Apr-2026 14:23:11 UTC] PHP Notice:  Undefined variable: foo in /var/www/html/wp-content/plugins/some-plugin/init.php on line 42
[08-Apr-2026 14:23:11 UTC] PHP Warning: include(): Failed opening '/var/www/html/missing.php' for inclusion in /var/www/html/wp-content/themes/my-theme/functions.php on line 17
[08-Apr-2026 14:23:12 UTC] PHP Fatal error:  Uncaught Error: Call to undefined function bar() in /var/www/html/wp-content/plugins/broken-plugin/main.php:15

Read each entry left to right. The bracketed timestamp is in UTC, not your local time. Then comes the error level, the message, the file path that fired the error, and the line number. The file path is the single most important field: it tells you immediately whether the problem is in WordPress core (wp-includes/, wp-admin/), a plugin (wp-content/plugins/<plugin-name>/), a theme (wp-content/themes/<theme-name>/), or a must-use plugin (wp-content/mu-plugins/).

The error level tells you how worried to be:

Level What it means Action
PHP Notice Non-critical: an undefined variable, missing array key, or similar low-grade issue Worth fixing, especially if it appears repeatedly. Often signals a plugin bug.
PHP Warning Something went wrong but execution continued Fix soon. The code did not crash but it almost certainly did not do what it should.
PHP Deprecated A function or feature being removed in a future PHP version Upgrade the plugin or theme. PHP 8.x and beyond will eventually break this code.
PHP Fatal error Execution stopped Fix immediately. This is what produced your white screen or critical error screen.
PHP Parse error Syntax error in the source code Fix immediately. Almost always a manual edit gone wrong in functions.php or wp-config.php.

When you have multiple errors and you are not sure which one matters, focus on the last PHP Fatal error or PHP Parse error before the failure. Notices and warnings can fill the log for weeks without breaking anything, but a fatal is what stopped the request.

To watch the log in real time while you reproduce the bug, on a server with shell access, run:

# Stream new lines as they are appended
tail -f wp-content/debug.log

Press Ctrl+C to stop. This is the fastest way to see exactly which file fires when you click a button or load a page.

Common error patterns and what they mean

A handful of patterns are common enough that recognising them shortcuts the diagnosis.

PHP Fatal error: Allowed memory size of N bytes exhausted. The script ran out of memory. Cause is almost always a plugin or import operation doing too much work in one request. The targeted fix path lives in allowed memory size exhausted in WordPress.

PHP Parse error: syntax error, unexpected token "...". A .php file is malformed, almost always from a manual edit. PHP refuses to execute the file at all. The diagnostic path lives in syntax error, unexpected: how to fix a PHP parse error in WordPress.

PHP Fatal error: Uncaught Error: Call to undefined function .... The named function does not exist at the moment it was called. Usually means a plugin or theme expects another plugin to be loaded first, the autoloader is broken, or a function was renamed across versions.

PHP Fatal error: Maximum execution time of N seconds exceeded. The script ran longer than the configured time limit. Often signals a slow database query or a runaway loop. Pair this with maximum execution time exceeded in WordPress.

PHP Warning: Cannot modify header information - headers already sent by .... Something printed output before WordPress tried to send an HTTP header. The file path in the message names where the unwanted output came from, which is almost always a stray space before <?php or after ?> in a theme or plugin file.

PHP Deprecated: Function X is deprecated. The named function will be removed in a future PHP version. Update the plugin or theme that uses it. This is not breaking your site today, but it will after the next PHP upgrade.

The thing all these patterns have in common: the file path in the message is the answer. WordPress core fires the error, but wp-content/plugins/<name>/... in the path is the plugin you need to update, disable, or replace.

Blocking public access to debug.log

wp-content/debug.log is inside your webroot by default. That means it is reachable at https://yoursite.nl/wp-content/debug.log from anywhere on the internet. Anyone who knows or guesses the URL can download the file, read your file paths, your database table names, fragments of database content captured in error messages, your plugin list, and sometimes usernames or session details. This is a real-world data leak and search engines have been known to index these files.

Always block public access before enabling logging, not after.

The simplest fix on Apache is a Files directive in the .htaccess file in your site root:


    Require all denied

That uses the modern Require syntax shipped in Apache 2.4. If you are on a much older Apache, the legacy form works too:


    Order Allow,Deny
    Deny from all

On Nginx, add a location block to your server config (you will need server access or a hosting panel that exposes Nginx config):

location = /wp-content/debug.log {
    deny all;
}

The cleanest fix, if your host allows it, is to move the log out of the webroot entirely using the string-path form of WP_DEBUG_LOG:

define( 'WP_DEBUG_LOG', '/home/user/private-logs/wp-errors.log' );

A path outside the docroot cannot be requested over HTTP at all, so no .htaccess rule is needed. This is the approach I use on every site I run, because it removes the failure mode where someone forgets the .htaccess rule or it gets overwritten by a plugin update.

Verify the protection by opening https://yoursite.nl/wp-content/debug.log in an incognito browser window after enabling logging. You will know it worked when you get a 403 Forbidden or 404 Not Found instead of the file contents. Do this every time, before you walk away from a debugging session.

Keeping debug logging off in production

Debug logging is a development and incident-response tool. It should not be left on permanently on a live site. It writes a file on every request that has an error. It reveals information about your stack to anyone who can read the log. It can even slow your site down if a noisy plugin floods the log with notices on every page load.

When you have finished debugging, switch wp-config.php to the production-safe configuration:

// Production: explicit, safe defaults
define( 'WP_DEBUG', false );
define( 'WP_DEBUG_LOG', false );
define( 'WP_DEBUG_DISPLAY', false );
@ini_set( 'log_errors', 'On' );
@ini_set( 'display_errors', 'Off' );

The two ini_set calls at the bottom belong to the production configuration, not the development one. They tell PHP itself to log errors to whatever log target the host has configured (typically a server-level error log) and never to print errors in the HTML response. Even if a plugin elsewhere flips a setting, these lines mean errors quietly land in the server log, where you or your host can read them after the fact.

The full set of constants wp-config.php accepts, beyond the four debug ones, is documented in my wp-config.php settings reference. If you are about to edit the file for the first time, that is the article to read first.

If your host blocks .htaccess edits, blocks SFTP, or makes editing wp-config.php impractical (some "drag and drop" hosting environments do exactly this), you may need to ask your host for direct file access before you can apply any of the steps in this article.

Alternative: Query Monitor for development

If you are debugging on a staging environment and you would rather see errors in the admin toolbar than tail a log file, Query Monitor by John Blackbourn is the standard tool. Version 4.0.1, released on 2026-04-07, runs on WordPress 6.9.4 and PHP 7.4 through 8.5, and has more than 200,000 active installations.

Query Monitor hooks directly into PHP's error handler, so it captures errors in real time on every page load and shows them in the admin toolbar with the calling stack and the responsible plugin or theme. Critically, it does this without requiring WP_DEBUG = true and without needing WP_DEBUG_DISPLAY at all. Errors are visible only to logged-in administrators, never to visitors. That makes it safer than WP_DEBUG_DISPLAY on a staging site that anonymous users can reach, and faster to use than tailing a log file when you are clicking through admin pages.

Query Monitor and WP_DEBUG_LOG are not mutually exclusive. I run both simultaneously when chasing a tricky bug: Query Monitor for the live view of what is happening as I click around, the debug log for the historical record I can grep through afterwards.

A few cautions. Do not install Query Monitor on a production site. It adds measurable overhead on every request, and even though it tries to hide its data from non-administrators, the safest position is to keep it out of production entirely. It is a development tool. Disable it (or ideally uninstall it) before you are done. The same goes for SAVEQUERIES, the WordPress constant that captures every database query in $wpdb->queries for inspection: useful in development, expensive in production.

When the debug log shows that the problem is really a fatal in wp-config.php or a must-use plugin that crashes before WordPress can render its critical error screen, the diagnosis path lives in White Screen of Death in WordPress. When the debug log shows the critical error screen is firing as designed but you want the recovery flow explained, there has been a critical error on this website is the right next stop.

Want WordPress hosting that stays stable?

I handle updates, backups, and security, and keep performance steady—so outages and slowdowns don't keep coming back.

Explore WordPress maintenance

Search this site

Start typing to search, or browse the knowledge base and blog.