130 —WordPress
wp-config.php debug constants: the nine we always set
A site lands in your inbox: SFTP creds, vague symptoms, no documentation. Before reading a line of someone else's plugin code, we set nine wp-config constants.
A site lands in your inbox. SFTP creds, vague symptoms, no documentation: "the search bar throws a white screen on mobile but works on desktop sometimes." You connect, open wp-config.php, and before reading a single line of someone else's plugin code, you set nine constants. The site behaves differently for the next hour, and so does the audit trail. This is the cheatsheet.
The two we set before anything else
Two constants do most of the work on a freshly inherited legacy site. They turn logging on without leaking it to the public, and they freeze the surface most likely to make things worse while you read the codebase.
WP_DEBUG_LOG, with a real path
WP_DEBUG on its own just toggles PHP's error_reporting. The useful constant is WP_DEBUG_LOG, which writes those errors to a file. Its default location is /wp-content/debug.log, which is publicly readable on most hosts unless the previous developer remembered to block it. Don't trust them. Point the log at a path above the docroot.
define( 'WP_DEBUG', true );
define( 'WP_DEBUG_LOG', '/home/site/private/wp-errors.log' );
define( 'WP_DEBUG_DISPLAY', false );
@ini_set( 'display_errors', 0 );
That last ini_set matters. WP_DEBUG_DISPLAY can be undone by PHP if a plugin calls ini_set('display_errors', 1) later in the request. The pair belongs together. The WordPress debug docs have the canonical writeup.
DISALLOW_FILE_EDIT, immediately
One line of dashboard hardening that blocks a surprising number of post-compromise attacks against legacy WordPress: the in-dashboard theme and plugin editors. With them on, a stolen admin session can paste PHP into functions.php live and own the site in two clicks. Lock them.
define( 'DISALLOW_FILE_EDIT', true );
The seven that follow
The first two are non-negotiable. The next seven we set with judgement, in roughly this order.
SCRIPT_DEBUG
Tells WordPress to load the unminified core CSS and JS. When an admin script breaks because a plugin enqueued jQuery twice, you can read the line that broke it instead of squinting at wp-admin.min.js:1:14238.
define( 'SCRIPT_DEBUG', true );
SAVEQUERIES
Every database query made during a request gets stored on $wpdb->queries, including the SQL, the time, and the calling function. It is heavy. Don't leave it on in production. But for an afternoon of "why does this page take 11 seconds," nothing else gets you closer to the answer.
define( 'SAVEQUERIES', true );
DISALLOW_FILE_MODS
The bigger hammer. Blocks plugin and theme installs, updates, and deletes from the dashboard, on top of the editor lockdown. We set this on every inherited site until we've decided which auto-update path it's on. It is the difference between "investigating a breach" and "investigating a breach while the attacker is still installing plugins." The wp-config.php reference documents the exact behaviour, and it's worth reading once.
define( 'DISALLOW_FILE_MODS', true );
WP_AUTO_UPDATE_CORE
Three useful values: true (all updates), 'minor' (security and minor only, the WordPress default), and false (nothing). On an unfamiliar site, set it to 'minor' so security patches still land while you read the codebase. Don't leave it on false and forget; nobody else will remember.
define( 'WP_AUTO_UPDATE_CORE', 'minor' );
FORCE_SSL_ADMIN
If the site has a valid certificate, and in 2026 it does, this forces every admin request and every auth cookie over HTTPS. Sites that survived from the cPanel-and-shared-IP era often haven't set it, and the login form is still POSTing over port 80 through a CDN that "upgrades" silently. Make it explicit.
define( 'FORCE_SSL_ADMIN', true );
WP_POST_REVISIONS
Without a cap, a chatty editor can leave a single page with 600 revision rows. Cap it. Five is generous for most editorial workflows.
define( 'WP_POST_REVISIONS', 5 );
WP_MEMORY_LIMIT and WP_MAX_MEMORY_LIMIT
Counted as one because we never set them separately. WP_MEMORY_LIMIT is the front-end ceiling; WP_MAX_MEMORY_LIMIT is the admin one (imports, media, WP-CLI). Bump both before debugging memory-fatal plugins, otherwise you'll spend an hour chasing a fatal that the host's php.ini already capped from underneath.
define( 'WP_MEMORY_LIMIT', '256M' );
define( 'WP_MAX_MEMORY_LIMIT', '512M' );
The full block, copy-pasteable
Drop this above the /* That's all, stop editing! */ line. Keep your real DB credentials and table prefix above it.
/* --- inherited-site debug block --- */
define( 'WP_DEBUG', true );
define( 'WP_DEBUG_LOG', '/home/site/private/wp-errors.log' );
define( 'WP_DEBUG_DISPLAY', false );
@ini_set( 'display_errors', 0 );
define( 'SCRIPT_DEBUG', true );
define( 'SAVEQUERIES', true );
define( 'DISALLOW_FILE_EDIT', true );
define( 'DISALLOW_FILE_MODS', true );
define( 'WP_AUTO_UPDATE_CORE', 'minor' );
define( 'FORCE_SSL_ADMIN', true );
define( 'WP_POST_REVISIONS', 5 );
define( 'WP_MEMORY_LIMIT', '256M' );
define( 'WP_MAX_MEMORY_LIMIT', '512M' );
/* --- end debug block --- */
What to undo before you hand it back
Three of the nine should not stay on past the debugging window:
- SAVEQUERIES off. It allocates memory for every query on every request.
- SCRIPT_DEBUG off. Unminified core assets are slower for visitors.
- WP_DEBUG and WP_DEBUG_LOG off, or at least pointed at a rotated path. A debug log that grows for six months is its own incident.
The other six (DISALLOW_FILE_EDIT, DISALLOW_FILE_MODS, WP_AUTO_UPDATE_CORE, FORCE_SSL_ADMIN, WP_POST_REVISIONS, the memory limits) stay on. They cost nothing and they keep paying out.
The mu-plugin that makes SAVEQUERIES useful
SAVEQUERIES on its own just stores data on $wpdb. To read it, drop a one-file mu-plugin at /wp-content/mu-plugins/000-savequeries-dump.php:
<?php
add_action( 'shutdown', function () {
if ( ! defined( 'SAVEQUERIES' ) || ! SAVEQUERIES ) return;
if ( ! current_user_can( 'manage_options' ) ) return;
global $wpdb;
$total = 0;
foreach ( $wpdb->queries as $q ) $total += $q[1];
error_log( sprintf(
'[savequeries] %d queries, %.3fs total on %s',
count( $wpdb->queries ),
$total,
$_SERVER['REQUEST_URI'] ?? 'cli'
) );
} );
Now every admin pageview logs its query count and total DB time to the same file WP_DEBUG_LOG is writing to. Tail it while you click around the dashboard, and the slow query announces itself.
Where Pier fits
When we built Pier for working on inherited WordPress sites, this list was the first thing we wired into the connect flow. The app reads wp-config.php on dock, flags the constants that are missing or set to risky values, and offers a one-click patch. Every change to the file goes into version history, so the block above and the block underneath are both rollbackable without grepping an SFTP backup.
Open the next wp-config.php you inherit, grep for these nine constants, and write down which ones are missing. That's the audit. Setting them is the easy part.
— Questions —
Where should WP_DEBUG_LOG point to?
Above the docroot, ideally outside any web-served directory. The default /wp-content/debug.log is publicly readable on most shared hosts unless something explicitly blocks it.
Does DISALLOW_FILE_EDIT also block plugin installs?
No. DISALLOW_FILE_EDIT only blocks the in-dashboard code editor for themes and plugins. To block installs, updates, and deletes as well, set DISALLOW_FILE_MODS to true.
Is SAVEQUERIES safe to leave on in production?
No. It allocates memory for every query on every request and can balloon memory use on long-running admin pages. Turn it on for a debugging window, then turn it back off.
What's the difference between WP_MEMORY_LIMIT and WP_MAX_MEMORY_LIMIT?
WP_MEMORY_LIMIT is the ceiling for front-end requests. WP_MAX_MEMORY_LIMIT is the ceiling for the admin, WP-CLI, and import operations. Both are capped by the host's php.ini.