WordPress Development

wp-config.php Explained: The Constants That Earn Their Place

wp-config.php is the first file WordPress reads that is specific to your install, and almost every fix you will ever be handed ends with "add this line to wp-config.php". Here is what the file actually does, which constants earn their place, and why the placement rule matters.

wp-config.php Explained: The Constants That Earn Their Place

Open a WordPress install over SFTP and the root directory looks like machinery: wp-admin, wp-includes, and a scattering of loose PHP files you have been told never to touch. One file in that list is different. wp-config.php is the only file in the root that is about your site rather than about WordPress.

It is about a hundred lines, and the installer wrote most of them for you. It has no settings screen, no admin page and no undo. It is also where almost every piece of WordPress troubleshooting advice eventually lands: turn on debugging, raise the memory limit, cap revisions, lock the file editor. Each of those is one line in this file.

The part people miss is that it is plain executable PHP. A stray character in it takes the whole site down as completely as a broken theme file, and for the same reason.

Boot order diagram showing wp-config.php loading before the database, plugins and theme, with wp-settings.php as the point after which added constants no longer take effect

What the file is and when it runs

Every front-end request starts at index.php in the WordPress root, which requires wp-blog-header.php, which requires wp-load.php. Admin requests come in through wp-admin/admin.php, which requires the same wp-load.php. That file has one job: find wp-config.php and require it. If there is no config file to find, it sends you to wp-admin/setup-config.php, which is the “There doesn’t seem to be a wp-config.php file” screen from a fresh install.

So wp-config.php is the first thing WordPress reads that knows anything about your particular site. Almost nothing of WordPress exists yet at that moment: no database connection, no plugins, no theme, no hooks. There is a PHP file defining constants, and then, on its last meaningful line, a require_once ABSPATH . 'wp-settings.php'; that boots everything else.

That sequence explains the whole design. The constants you set here are not settings WordPress reads back from a table later. They are values that already exist in memory by the time the code that cares about them runs. wp-settings.php calls wp_initial_constants() in wp-includes/default-constants.php, and that function is a long list of if ( ! defined( 'X' ) ) define( 'X', $default ); statements. Your definition wins because it got there first. That is the entire mechanism.

<?php
/** Database connection */
define( 'DB_NAME', 'example_db' );
define( 'DB_USER', 'example_user' );
define( 'DB_PASSWORD', 'a-long-random-password' );
define( 'DB_HOST', 'localhost' );
define( 'DB_CHARSET', 'utf8mb4' );
define( 'DB_COLLATE', '' );

/** Authentication keys and salts - yours are different, and should be */
define( 'AUTH_KEY',        'long random string' );
define( 'SECURE_AUTH_KEY', 'long random string' );
/* ...six more... */

$table_prefix = 'wp_';

/* Add any custom values between this line and the "stop editing" line. */

define( 'WP_DEBUG', false );

/* That's all, stop editing! Happy publishing. */

if ( ! defined( 'ABSPATH' ) ) {
	define( 'ABSPATH', __DIR__ . '/' );
}

require_once ABSPATH . 'wp-settings.php';

The stop editing line

Every guide tells you to put your line above the /* That's all, stop editing! */ comment and almost none of them say why. The comment itself is decoration; older installs say “Happy blogging” instead of “Happy publishing” and neither wording does anything. What matters is the require_once immediately below it.

wp-settings.php does not return until WordPress has loaded core, connected to the database, run must-use plugins, run every active plugin, set up the theme and fired wp_loaded on its final line. Anything you write after that require executes when the site is already fully assembled. A define( 'WP_DEBUG', true ) down there is legal PHP and produces no error, but wp_debug_mode() read the constant near the top of wp-settings.php and moved on. The line simply does nothing.

One related trap: PHP constants cannot be redefined. If a constant is already defined earlier in the file, a second define() for it is ignored, the first value stands, and PHP emits a warning that you will probably never see because nothing is displaying it. When a constant you added stubbornly refuses to take effect, search the file for a second copy before you assume anything more exotic.

Side by side comparison of a WP_DEBUG constant placed above and below the stop editing line in wp-config.php, showing only the first one takes effect

The four database constants

DB_NAME, DB_USER, DB_PASSWORD and DB_HOST are the credentials wpdb uses to open the connection, and they are the only four values in the file that WordPress cannot function without. DB_CHARSET is utf8mb4 on any modern install and DB_COLLATE is almost always left empty.

Three of those four are usually correct and stay correct. DB_HOST is the one that moves: localhost on most shared hosting, an internal hostname or IP address on managed platforms, and sometimes a host with a port or a socket path appended. If a site that worked yesterday is now showing a connection error, that is a separate diagnosis with its own order of operations, and the error establishing a database connection walkthrough covers it properly.

Keys and salts

The eight long random strings in the middle of the file are the least understood part of it. They are not passwords and nobody ever types them. They are secret inputs to WordPress’s hashing, supplied through wp_salt() in wp-includes/pluggable.php, and they come in four pairs, one per scheme:

  • AUTH_KEY and AUTH_SALT key the standard authentication cookie.
  • SECURE_AUTH_KEY and SECURE_AUTH_SALT key the authentication cookie sent over HTTPS.
  • LOGGED_IN_KEY and LOGGED_IN_SALT key the cookie that identifies you as signed in without granting admin access.
  • NONCE_KEY and NONCE_SALT key the nonces attached to admin forms and action URLs.

A WordPress login cookie is a signed token, not a stored secret. wp_generate_auth_cookie() derives a key by hashing the username, a four-character fragment of the stored password hash, the expiry and the session token together with the relevant key and salt, then builds an HMAC of the username, expiry and token with it. On the next request WordPress recomputes that HMAC and compares. If the secret changes, every previously issued cookie fails verification. Nonces work the same way through wp_create_nonce().

This is why WordPress.org runs a secret-key service at api.wordpress.org/secret-key/1.1/salt/: it returns eight ready-made define() lines with random values, so nobody has to invent randomness by mashing the keyboard. If the constants are missing, or still hold the put your unique phrase here placeholder from wp-config-sample.php, wp_salt() generates values with wp_generate_password() and stores them as site options instead. The site works, but the secret now lives in the database rather than in a file, and anything that can read the database can forge a cookie.

Replacing all eight lines logs out every user on the site, including you, and invalidates every outstanding nonce. That is not a side effect to be worked around. It is the point. If you suspect a compromise, rotating the salts is the correct and immediate way to kill every session an attacker might be holding, and it should be done alongside changing passwords rather than instead of it.

The table prefix

$table_prefix is the odd one out: a plain PHP variable rather than a constant. Core hands it to wpdb::set_prefix() from wp_set_wpdb_vars() in wp-includes/load.php, and every table name the class exposes is built from it. The default is wp_, which is why you see wp_posts and wp_options everywhere. It may contain letters, numbers and underscores only; anything else and WordPress stops with an error naming the variable and the file.

Choosing a different prefix at install time is free, and there is one genuinely good reason for it: several WordPress installs can then share a single database without colliding. The security argument is thinner than its popularity suggests. It raises the cost of a blind SQL injection that hardcodes table names, and that is all it does. It does not stop anything that can read wp-config.php, and it does not stop an attacker who can query information_schema.

Changing it on a live site is a migration, not a tweak. Every table has to be renamed, then the option_name row storing wp_user_roles and the meta_key rows storing wp_capabilities and wp_user_level have to be renamed to match, plus whatever prefixed keys plugins have written. Get one wrong and nobody, including you, has any capabilities any more. If the site is already running, the honest answer is to leave the prefix alone and spend the effort on updates and strong passwords.

Debug constants

WP_DEBUG defaults to false, with one exception worth knowing: core turns it on by itself when WP_ENVIRONMENT_TYPE is development or a development mode is set. Define it as true and wp_debug_mode() in wp-includes/load.php sets PHP’s error reporting to E_ALL and lets WordPress emit its own deprecation and “doing it wrong” notices. It is the single most useful line in the file when something is broken.

It has two companions, and both are inert on their own because wp_debug_mode() only reads them inside its if ( WP_DEBUG ) branch. WP_DEBUG_LOG defaults to false; set it to true and errors are appended to wp-content/debug.log. Since WordPress 5.1 you can give it a file path string instead, which is the better choice, because the default log file sits inside the web root and is readable by anyone who guesses the URL. WP_DEBUG_DISPLAY defaults to true, meaning errors are printed into the page. On a live site you want it false.

The combination worth knowing is debugging on, display off, log to a path outside the web root. You get the full error text without showing visitors a stack trace and without publishing your file paths. Core hides errors from Ajax, REST and XML-RPC requests regardless, so a broken block editor will still fail silently in the browser and tell you the truth in the log. This is also the fastest way to turn a blank page into a readable message, which is where the white screen of death guide starts.

/* Investigate quietly: log everything, show nothing. */
define( 'WP_DEBUG', true );
define( 'WP_DEBUG_LOG', 'https://cdn.wp-image-editor.com/home/youraccount/logs/wp-errors.log' );
define( 'WP_DEBUG_DISPLAY', false );

The two memory constants

WP_MEMORY_LIMIT is the baseline WordPress asks PHP for on a normal request. Left undefined, core sets it to 40M on a single site and 64M on multisite. WP_MAX_MEMORY_LIMIT is the higher ceiling core raises to for memory-hungry work, and it defaults to 256M, or to PHP’s own memory_limit when that is already higher. They are not a minimum and a maximum of the same thing, and they are not interchangeable.

The mechanism is wp_raise_memory_limit(), which core calls with a context: admin from wp-admin/admin.php when you load the dashboard, image from WP_Image_Editor_GD and WP_Image_Editor_Imagick when one of them opens a file to resize or crop, cron from wp-cron.php. In those contexts WP_MAX_MEMORY_LIMIT applies instead of the baseline. That is why an upload can fail while the rest of the site is untroubled, and it is why raising only WP_MEMORY_LIMIT sometimes changes nothing about a failing image resize.

Two honest caveats. Core only ever raises, never lowers: it calls ini_set() only when the value you asked for is larger than the current one, so setting a small number here will not reduce anything. And if the host has locked memory_limit so that ini_set() cannot change it, wp_raise_memory_limit() checks wp_is_ini_value_changeable() and gives up before it tries. Your lines stay in the file and change nothing, and the fix is a conversation with the host. The memory limit article works through how to tell which of those situations you are in.

Revisions, autosaves and the trash

WP_POST_REVISIONS defaults to true, which wp_revisions_to_keep() reads as unlimited. Core stores a revision only when a revisioned field actually changed, but on a site with long editorial cycles the posts table still fills up with full copies of everything anyone has ever edited. Set the constant to an integer and only that many most-recent revisions are kept. Setting it to false disables revisions entirely, which is a real loss the first time somebody needs to undo a bad edit. Ten is a reasonable number for most sites, and while a bloated posts table is worth trimming, it is rarely the headline answer when a WordPress site feels slow.

AUTOSAVE_INTERVAL defaults to 60 seconds, defined as MINUTE_IN_SECONDS. Core passes the value to the editor, which uses it to decide how often to save behind you, and each of those saves is another request to the server. Raising it to 120 or 180 trims a small amount of background traffic while you write and costs you at most a couple of minutes of work if the browser dies. It is a minor tuning knob, not a performance fix.

EMPTY_TRASH_DAYS defaults to 30. A daily scheduled task, wp_scheduled_delete(), permanently removes trashed posts and comments once they have sat there that long. Lowering it to 7 or 14 keeps things tidier. Setting it to 0 does something more drastic than it sounds: it turns the trash off altogether, and wp_trash_post() deletes permanently instead, so “Move to Trash” becomes an immediate delete with no recovery step. Very few sites want that.

Turning off the built-in file editor

DISALLOW_FILE_EDIT is off by default, and turning it on is one of the few genuinely worthwhile hardening lines that costs nothing. With it set to true, Appearance → Theme File Editor and Plugins → Plugin File Editor disappear. The enforcement is not cosmetic: map_meta_cap() in wp-includes/capabilities.php maps edit_themes, edit_plugins and edit_files to do_not_allow when the constant is true, so the capability is gone no matter how the request arrives.

The threat it removes is specific. Anyone who obtains an administrator session on a site with the editor enabled can write arbitrary PHP into a theme file from a browser, with no FTP access and no file upload. Closing that path means an attacker needs a second foothold. The cost to you is having to edit theme files the way you should be editing them anyway, over SFTP or in version control.

Its stronger sibling, DISALLOW_FILE_MODS, blocks all plugin and theme installation, updating and deletion from the dashboard, and switches off automatic updates along with them. That is appropriate for a deployment-managed site where code arrives through a pipeline. On an ordinary site it also blocks security updates, so use it deliberately or not at all.

Auto-updates and environment type

WP_AUTO_UPDATE_CORE has three values most sites should care about. 'minor' allows point releases, which are mostly security and bug fixes, and blocks major upgrades. true allows both. false turns off core auto-updates entirely, including the security releases, and unless something is watching the site and applying them by hand within days, that is a worse position than the default. Left undefined, a stable install behaves like 'minor' unless someone has opted into major updates from Dashboard → Updates; defining the constant overrides that screen. It also accepts 'beta', 'rc' and 'development', which put the site on a pre-release channel and belong nowhere near production.

WP_ENVIRONMENT_TYPE arrived in WordPress 5.5 and accepts exactly four values: local, development, staging and production. Anything else is discarded and wp_get_environment_type() returns production. Core reads it in wp-includes/load.php, checking an environment variable of the same name first and then letting the constant override it, so a value in wp-config.php always wins over the server’s.

Core uses the value in a handful of places, mostly to relax a check outside production and to report the environment in Site Health. Its real purpose is to give plugins and themes one canonical answer to “which copy of this site am I?” so that a staging clone does not email real customers, charge real cards or fire analytics. Setting it explicitly on both production and staging is cheap insurance. The related WP_DEVELOPMENT_MODE constant, added in 6.3, is a different thing: it accepts core, plugin, theme or all and switches off caching of theme.json data and block metadata so your edits show up immediately. It belongs only on a machine where you are actively building.

Reference table of nine wp-config.php constants with their WordPress defaults and what each one changes, from WP_DEBUG to DISALLOW_FILE_EDIT

One block worth pasting

This goes above the stop-editing line. Every value is a defensible default rather than a maximum, and none of it is destructive.

/* Debugging: off, but wired up so you can flip one value when needed. */
define( 'WP_DEBUG', false );
define( 'WP_DEBUG_LOG', true );      /* inert until WP_DEBUG is true; swap for a path outside the web root */
define( 'WP_DEBUG_DISPLAY', false ); /* never print errors to visitors */

/* Memory. Raise further only if you have evidence you need to. */
define( 'WP_MEMORY_LIMIT', '128M' );
define( 'WP_MAX_MEMORY_LIMIT', '256M' );

/* Content housekeeping. */
define( 'WP_POST_REVISIONS', 10 );
define( 'AUTOSAVE_INTERVAL', 120 );
define( 'EMPTY_TRASH_DAYS', 14 );

/* Hardening: no editing plugin or theme code from the dashboard. */
define( 'DISALLOW_FILE_EDIT', true );

/* Updates and environment. */
define( 'WP_AUTO_UPDATE_CORE', 'minor' );
define( 'WP_ENVIRONMENT_TYPE', 'production' );

Two constants you will meet in other people’s fixes are worth recognising even if you do not add them. WP_HOME and WP_SITEURL override the home and siteurl options from the database, which is how people rescue a site that has redirected itself to the wrong domain after a move. Defining them also disables the address fields in Settings → General, so a later domain change has to happen in this file. DISABLE_WP_CRON stops WordPress from firing scheduled tasks on page loads, which only makes sense if you have replaced it with a real system cron job requesting wp-cron.php. Without that replacement, scheduled posts, backups and update checks silently stop.

Rather than copying that block and editing the values by hand, you can assemble your own. Tick the constants you actually want below and the builder writes the block, in a stable order, with the values quoted correctly.

It also watches for the combinations that cancel each other out: debug logging without debug, a memory limit set below the one PHP allows, file modifications disabled on a site that still needs security updates.

wp-config.php builder

Tick the constants your site needs, set their values, and get a ready wp-config.php block with comments and a live check for the combinations that quietly cancel each other out. Everything is worked out in this browser tab, nothing is sent anywhere.

Start from a template

Debugging

The master switch for PHP notices, warnings and deprecations.

Writes those messages to wp-content/debug.log, or to a path you choose.

Whether the messages are printed into the page output.

Loads the unminified core CSS and JavaScript.

Keeps every database query in memory for profiling. Costs memory.

Turns off recovery mode, so a fatal error is shown instead of the polite page.

Performance

Memory for the front end. The core default is 40M, or 64M on multisite.

Memory for admin work such as image handling and updates.

How many revisions a post keeps, or false to keep none.

Seconds between autosaves in the editor.

Days before the trash is emptied. 0 switches the trash off.

Stops the pseudo cron that runs on page views.

Seconds before a second cron run may start.

Merges the admin scripts into fewer requests.

Compresses the merged admin CSS before it is sent.

Compresses the merged admin JavaScript before it is sent.

Security

Removes the theme and plugin editors from the admin.

Blocks every install, update and delete, from the admin and from the API.

Sends the admin and the login form over https only.

Switches off all automatic background updates.

Which core updates install themselves: all, none, or minor only.

Addresses and paths

The address visitors type, with no trailing slash.

Where the WordPress files sit, with no trailing slash.

Absolute server path to the content directory.

The matching URL for that directory.

Upload folder, relative to the WordPress root, with no leading slash.

Where uploads are written while they are being received.

Your block

Paste it into wp-config.php above the line /* That's all, stop editing! Happy publishing. */

Further down the file it would be read too late: WordPress has set its defaults by then, so constants placed below that line quietly do nothing.

Nothing ticked yet
WunderPaint
The Dynamic Design and Automation Studio
WunderPaint is a layered image editor for your WordPress media library. WunderPaint Studio is the same thing in any browser, free and without an account.

What does not belong in wp-config.php

Functionality. No add_action(), no add_filter(), no custom functions, no redirects, no tracking scripts. The file is for configuration values that must exist before WordPress boots, and nothing else.

There is a hard technical reason as well as a stylistic one. add_action() lives in wp-includes/plugin.php, which is loaded early by wp-settings.php. Above the stop-editing line that function does not exist yet, and calling it produces an immediate fatal error and a blank site. Below the stop-editing line it exists, but the hook you are attaching to has usually already fired. Either way the code does not do what its author intended.

Site behaviour belongs in a small plugin, or in the theme’s functions.php when it is genuinely presentational and specific to that theme. The functions.php guide covers where the line falls and why a site-specific plugin usually beats the theme file. The test is simple: if switching themes should not turn the feature off, it does not belong in the theme, and if it needs WordPress to be loaded, it does not belong in wp-config.php.

Editing it without taking the site down

Download a copy before you change anything. Keep that copy off the server, or at least outside the web root, because a file named wp-config.php.bak or wp-config.txt sitting next to the original is no longer executed as PHP and will happily be served as plain text, database password and all. That is one of the more common ways credentials leak.

Edit it as PHP, with a real editor. A missing semicolon, a curly quote pasted in from a web page, or a single stray character before the opening <?php is enough to break every request the site receives. If your editor can run a syntax check, use it. There is no need for a closing ?> tag at the end of the file, and leaving it off avoids the whitespace-after-the-tag problem that produces “headers already sent” warnings.

If you cannot find the file in the WordPress root, look one directory above it. wp-load.php deliberately checks dirname( ABSPATH ) as a second location, on the condition that there is no wp-settings.php alongside it, so hosts can place the config file outside the web root where the web server will never serve it directly. That is a sound arrangement, not a broken install. Restrictive file permissions, commonly 640, are worth setting either way.

Symptoms and causes

The whole site went blank the moment I saved wp-config.php. That is a PHP parse error, and the file is loaded before anything that could handle it gracefully. Restore your copy, or fix the typo, and turn on error logging so the next mistake tells you the line number instead of showing you nothing.

I set WP_DEBUG to true and nothing changed. Either the line is below the stop-editing comment, where it is read too late to matter, or the constant is already defined higher up in the file and the first definition wins. Search the file for a second WP_DEBUG before assuming anything else.

Every user was logged out after I edited the file. The keys and salts changed, so every existing auth cookie now fails its HMAC check. Logging back in issues a new one. Nothing is lost, and if you rotated the salts on purpose after a security incident, this is confirmation that it worked.

I raised WP_MEMORY_LIMIT and uploads still fail. Image processing runs under WP_MAX_MEMORY_LIMIT, not the baseline, so raise that one too. If both are set and the limit still will not move, the host has made memory_limit unchangeable at runtime, and core stops trying rather than fighting it.

The site redirects to the wrong domain after a migration. The siteurl and home options in the database still point at the old address. Defining WP_HOME and WP_SITEURL overrides them immediately and gets you back into the dashboard, where the underlying URLs can be corrected properly.

What a good wp-config.php looks like

Short. The database credentials the installer wrote, eight random strings you generated once and never think about again, a table prefix you chose at install time and left alone, and somewhere between five and ten deliberate constants above the stop-editing line. Anything longer than that usually means something has crept in that should have been a plugin.

The reason this file is worth understanding once, properly, is that it is the endpoint of so much other advice. Debugging, memory, revisions, the file editor, environment awareness and the emergency URL override all land here, and they land here for the same reason: they have to be true before WordPress starts, not after. Once the load order makes sense, the placement rule stops being folklore and becomes obvious.

Treat it like the piece of code it is. Keep a copy before you touch it, change one thing at a time, and know what each line you added is for. A config file whose every line you can explain is a config file that will not surprise you at two in the morning.

wp-config.php Explained: The Constants That Earn Their Place

Table of Contents

Learn it by building something

Every week one thing you can make the same afternoon, from dynamic templates to 3D type. Written down step by step.

One mail a week, and then it ends.
Unsubscribe in one click.

Troubleshooting

WordPress Missed Schedule: Why WP-Cron Is Not a Cron Job

WP-Cron has no timer and no background process. It is a list of due tasks that gets checked only when somebody loads a page, which explains missed post schedules, stalled backups, and every other scheduled task that silently stops.

WordPress Images

How Big Should an Image Actually Be Before You Upload It

A 4032 pixel phone photograph in a 760 pixel column makes the browser download 12.2 million pixels to paint about 430,000 of them. What that costs in bytes and in memory, how to read the display width your theme actually uses, where the WordPress 2560 pixel safety net stops, and a browser tool that does the resizing before the upload.

Troubleshooting

Why Your WordPress Images Look Blurry, and Which Cause It Actually Is

Blurry WordPress images are not one problem but six, and each leaves a different fingerprint. Work out which symptom you have before you change anything, because most of the fixes do nothing for most of the causes.

Photo Editing

Remove an unwanted object from a photo without uploading it anywhere

A clone tool copies pixels from elsewhere. An inpainting model predicts what should have been behind the thing you removed, which is a different question with a much better answer on grass, hedges and brickwork. It runs in your browser, and the rest of the photograph comes back byte for byte unchanged.

Troubleshooting

WordPress HEIC Upload: What Core Does and Why It Fails

WordPress has allowed .heic uploads since 6.7 and converts them to JPEG on the way in, but only when the server has a HEIC-capable Imagick. Here is what core actually does with the file, why the same site answers differently through a browser and through WP-CLI, and which fix stops the problem instead of managing it.

Design Fundamentals

How to Choose a Color Palette Without Learning Color Theory

Palette generators hand you five swatches of equal weight, which is the one thing a real palette never is. Here is the ten minute version that works.

Download the free WunderPaint Plugin for WordPress

The WunderPaint workspace with the layers panel, adjustment sliders, text style presets and the asset library along the bottom

The Image Editor & Design Studio

Everything described here can be done in the browser, on your own site. The live demo runs the full editor with nothing to install.

Free

Chaos Art

Autonomous painters make one-of-a-kind abstract art in 3D space - gestures, art movements, painterly media, and embeds that paint a new original for every visitor.

Pro

Particle Strokes

Paint with swarms of light: twenty-two movements, a stamp you draw yourself, and curves that give a stroke a shape - the swarm keeps painting for a few seconds after you let go.

Pro

City Diorama

Any place on earth as a miniature you could hold: real streets, water and building footprints raised into a 3D diorama - or wrapped around a sphere as your own tiny planet.

Free

Papercut Art

Layered paper pictures with real depth - a photo sliced along its actual depth into up to twenty layers, parametric landscapes, animals and clouds you can shape, letters with real counters, and a look that runs on three dials.