— Article — № 056

056 —WordPress

wp-config.php debug constants: a working cheatsheet

Forty documented wp-config.php constants ship with WordPress. Most sites use three of them. Here are the rest, written as a working cheatsheet you can paste in tonight.

Overhead photo of aged wp-config.php cheatsheet with PHP debug constants, brass paperweight, ruler, pencil, red wax seal.
Hero · staged still№ 056

At 23:41 last Thursday, an agency lead forwarded a Loom: WordPress 6.4 on a Hetzner VPS throwing a 500 on /wp-admin, blank page on the front, error log empty. The site had been running for three years. Their fix path was the usual loop. Disable plugins by renaming the folder over SFTP. Bisect. Re-enable. Hope.

The faster path was already sitting in their wp-config.php, or rather, sitting absent from it. WordPress has roughly forty documented configuration constants, and most production sites ship with three of them. The rest are the difference between a flaky WordPress install and a debuggable one. This is the working cheatsheet for the wp-config.php constants that actually earn their bytes.

A debug block that survives production

The classic mistake is define('WP_DEBUG', true) on a live site. PHP notices and deprecations stream into the rendered HTML, breaking JSON endpoints, AJAX, and anyone running a strict CSP. The wp-config.php constants that make debugging safe on production look like this:

define('WP_DEBUG', true);
define('WP_DEBUG_LOG', '/var/log/wp/debug.log');
define('WP_DEBUG_DISPLAY', false);
@ini_set('display_errors', '0');

WP_DEBUG_LOG accepts a path, not just a boolean. Point it outside the web root. The default location is wp-content/debug.log, which is web-accessible on any host that doesn't already deny dotfiles and *.log via .htaccess. The path you pass in needs to be writable by the PHP-FPM user, not your SFTP user.

WP_DEBUG_DISPLAY being explicitly false matters even when display_errors is already off at the PHP level, because some themes call ini_set in functions.php during bootstrap and undo it.

SAVEQUERIES for the slow page that won't reproduce locally

When a page takes 14 seconds on staging but 400ms on your laptop, the question is which query. SAVEQUERIES tells WordPress to keep every query, its execution time, and the call stack that triggered it inside $wpdb->queries.

define('SAVEQUERIES', true);

Then in a must-use plugin or a shutdown hook, dump the slow ones to the same log:

add_action('shutdown', function () {
    global $wpdb;
    if (!current_user_can('manage_options')) return;
    $slow = array_filter($wpdb->queries, fn($q) => $q[1] > 0.05);
    usort($slow, fn($a, $b) => $b[1] <=> $a[1]);
    error_log(print_r(array_slice($slow, 0, 10), true));
});

Turn it off again in production. SAVEQUERIES roughly doubles memory pressure per request and is the kind of constant that gets left on for six months and shows up in a profiling session as a mystery overhead.

Memory ceilings the dashboard hides

WordPress has two memory constants. WP_MEMORY_LIMIT applies to the front of the site. WP_MAX_MEMORY_LIMIT applies to /wp-admin only. Both can be overridden by a lower memory_limit in php.ini, which is the source of about half the "but I set it to 512M" support tickets.

define('WP_MEMORY_LIMIT', '256M');
define('WP_MAX_MEMORY_LIMIT', '512M');

The symptom of hitting the front-end ceiling is a half-rendered page that cuts off mid-footer with no error in the log, because PHP died before the shutdown handler ran. If the admin ceiling is the one being hit, you get a white screen on the plugins page or partway through a bulk action. Both look like "the site is just broken" to a non-developer.

The fatal-error handler that hides the real error

Since WordPress 5.2, a fatal error in a plugin gets caught by the recovery-mode handler, which emails the admin a magic link and renders a generic "There has been a critical error on this website" page. This is good for end users and miserable for developers, because it swallows the actual stack trace from the response body.

define('WP_DISABLE_FATAL_ERROR_HANDLER', true);

Turn this on while debugging. The white screen of death is more useful than a polite apology, because at least your debug log gets the trace. Turn it back off before you hand the site back to the client.

FS_METHOD and the FTP credentials prompt

When WordPress tries to update a plugin and the file owner doesn't match the PHP-FPM user, the admin pops up asking for FTP credentials. This is almost never what anyone wants on a modern host. Force direct filesystem access:

define('FS_METHOD', 'direct');

If this fails with "Could not create directory", the ownership is wrong. chown -R www-data:www-data wp-content usually fixes it, but check what your stack actually runs PHP as before copying that line. On nginx with PHP-FPM pools, the user is often something less obvious like php-fpm-site1.

A working baseline

Drop this block above the /* That's all, stop editing! Happy publishing. */ comment in wp-config.php. Adjust paths and limits to match your host.

// Debug, scoped to logs only
define('WP_DEBUG', true);
define('WP_DEBUG_LOG', '/var/log/wp/debug.log');
define('WP_DEBUG_DISPLAY', false);
@ini_set('display_errors', '0');

// Profile only when needed
define('SAVEQUERIES', false);

// Memory
define('WP_MEMORY_LIMIT', '256M');
define('WP_MAX_MEMORY_LIMIT', '512M');

// Updates and filesystem
define('FS_METHOD', 'direct');
define('DISALLOW_FILE_EDIT', true);
define('AUTOMATIC_UPDATER_DISABLED', false);
define('WP_AUTO_UPDATE_CORE', 'minor');

// Revisions and trash, kept bounded
define('WP_POST_REVISIONS', 10);
define('EMPTY_TRASH_DAYS', 14);

// SSL on the admin, only after the cert is verified working
define('FORCE_SSL_ADMIN', true);

What we ended up doing

When we built Pier for editing legacy sites by chat, the wp-config.php round-trip kept biting us. People would toggle WP_DEBUG on to chase a bug, forget about it, ship the site with display_errors leaking PHP notices into the JSON API, then six months later wonder why their iOS app couldn't parse the response. The way we ended up handling it was to surface the active config block in the file tree with a soft warning when debug constants are live, and to keep version history on every wp-config.php edit so you can roll back the one-liner that broke the cron job at 2 AM.

The smallest thing you can do today: open your wp-config.php, add the four-line debug block above, point WP_DEBUG_LOG at a path outside the web root, and tail it for an hour. You will be surprised what was already broken.

— Questions —

Does WP_DEBUG slow down production?

Marginally, if WP_DEBUG_LOG points to a fast disk and WP_DEBUG_DISPLAY is false. The cost is one filesystem write per notice, negligible until you also enable SAVEQUERIES.

Where should WP_DEBUG_LOG write to?

A path outside the web root, owned by the PHP-FPM user. /var/log/wp/debug.log is conventional. Never leave it at the default wp-content/debug.log on a public host.

Is DISALLOW_FILE_EDIT enough hardening?

It blocks the dashboard editor as a webshell vector. Pair it with DISALLOW_FILE_MODS to also block plugin installs, tight PHP-FPM file permissions, and a real WAF in front.

Why does WP_MEMORY_LIMIT seem ignored?

A lower memory_limit in php.ini or a per-pool PHP-FPM config will override it. WordPress can raise its own limit only up to whatever PHP itself allows for that process.