WordPress keeps its address in two rows of wp_options, called home and siteurl. Changing the site URL means writing both of them, plus every URL already baked into your post content.
Most guides give you four methods and do not say which of those three things each method covers. That is the gap, and it is why a URL change so often half-works.
The two rows do different jobs
home is the public address. siteurl is where the WordPress files live.
On most installs they are identical, which is exactly why people treat them as one setting. They are read by different functions and they break different things.
home is read by get_home_url(), which backs home_url(), permalinks, and the canonical redirect. siteurl is read by get_site_url(), which backs site_url() and admin_url().
The asset path is the part worth remembering. In wp-includes/default-constants.php, core defines:
define( 'WP_CONTENT_URL', get_option( 'siteurl' ) . '/wp-content' );
So every plugin and theme stylesheet, script and uploaded image URL is derived from siteurl. Nothing in that chain looks at home.
Which row is wrong, read from the symptom
This is the diagnostic that no guide on the first page states, and it saves the guesswork.
The front end redirects away or loops, but /wp-admin/ still loads. That is home: redirect_canonical() sends a 301 to home_url( '/' ).
At line 63 of wp-includes/canonical.php the whole function bails early on is_admin(), so the admin is never canonical-redirected and survives a wrong home.
The admin loads unstyled, or login bounces to the wrong host, but the front end is fine. That is siteurl. The admin CSS and JS come from WP_CONTENT_URL and includes_url(), both built from siteurl, so they resolve to a host that is not serving them.
Both are broken. Both rows are wrong, which is the normal state after a domain move that nobody finished.
Method 1: Settings, General
Two fields, "WordPress Address (URL)" is siteurl and "Site Address (URL)" is home.
This is the only method that writes both rows in one action, and it is the right choice whenever you can still reach the admin.
The catch is the order of operations. The moment you save, the redirect you are relying on changes, so if the new URL is not already serving your install you have locked yourself out of the screen you would use to undo it.
Method 2: WP-CLI, which is the one to prefer
wp option get home
wp option get siteurl
wp option update home 'https://example.com'
wp option update siteurl 'https://example.com'
WP-CLI does not care whether the site is reachable over HTTP, because it loads WordPress from the filesystem. There is no lockout to get locked out of.
That makes it the only method on this list that is safe to run on a site you have already half-broken. wp is preinstalled on every SandyWP sandbox, so you can rehearse the exact two commands on a copy before running them anywhere that matters (see SSH and SFTP access).
No trailing slash. Core stores these rows unslashed and comparisons elsewhere assume that.
Method 3: wp-config.php constants, which do not change anything
This is the method most often recommended and most often misunderstood.
define( 'WP_HOME', 'https://example.com' );
define( 'WP_SITEURL', 'https://example.com' );
These constants are not a write, they are read filters.
From wp-includes/functions.php:
function _config_wp_home( $url = '' ) {
if ( defined( 'WP_HOME' ) ) {
return untrailingslashit( WP_HOME );
}
return $url;
}
That function is hooked to option_home in default-filters.php, and _config_wp_siteurl to option_siteurl. They intercept the value on the way out of get_option() and hand back the constant instead.
The database rows are never touched. They still hold the old URL, and they will keep holding it for as long as the install exists.
So a constant is an override, not a fix. Remove it a year later, during a wp-config.php cleanup or a host migration that regenerates the file, and the site snaps back to a domain you stopped owning.
Worse, the admin actively hides this from you. The Settings field is rendered with form_option( 'siteurl' ), which goes through get_option() and therefore through the filter, so the greyed-out box shows you the constant while the stale row sits behind it.
Use constants for what they are good at: forcing a known-correct URL on an install you cannot otherwise boot, long enough to run Method 2 and then delete them.
Why the fields grey out, and why editing the HTML will not help
In wp-admin/options-general.php, both inputs get disabled( defined( 'WP_SITEURL' ) ) when the matching constant exists.
A disabled input is not submitted, so the save sends nothing for those fields. If you were thinking of removing the attribute in dev tools, wp-admin/options.php closes that too:
if ( ! defined( 'WP_SITEURL' ) ) {
$allowed_options['general'][] = 'siteurl';
}
if ( ! defined( 'WP_HOME' ) ) {
$allowed_options['general'][] = 'home';
}
With the constant defined, the option name never enters the allow-list, so a hand-crafted POST is discarded. The lock is real, in two places.
Method 4: RELOCATE, which only moves one row
define( 'RELOCATE', true );
Then load /wp-login.php. From line 507 of wp-login.php:
if ( defined( 'RELOCATE' ) && RELOCATE ) { // Move flag is set.
$url = dirname( set_url_scheme( 'http://' . $_SERVER['HTTP_HOST'] . $_SERVER['PHP_SELF'] ) );
if ( get_option( 'siteurl' ) !== $url ) {
update_option( 'siteurl', $url );
}
}
Three things follow from those lines that the short explanations leave out.
It writes siteurl only. home is untouched, so after a relocate the admin works and the front end can still be pointing at the old domain.
It guesses the value from the request, specifically the directory containing the login script. Hit /wp-login.php on the wrong hostname once, through a staging domain or a load balancer that rewrites Host, and it writes that hostname to the database.
It is not a one-shot: the check runs on every login page load while the constant exists, so it will keep overwriting siteurl.
Remove it as soon as you can log in.
The rows are only half the job
Changing home and siteurl does nothing to the URLs already stored in your content: post bodies, widget options, serialized theme settings, menu items.
That is a separate pass, and the tool is wp search-replace:
wp search-replace 'https://old.example.com' 'https://example.com' --dry-run
It is the step with the most ways to go quietly wrong, including tables with no primary key and serialized objects it cannot rehydrate. We wrote those up separately in what WP-CLI search-replace skips and how to check it did not corrupt serialized data.
The HTTP to HTTPS asymmetry
If you are changing scheme rather than domain, the two functions do not behave the same way.
get_home_url() forces https whenever is_ssl() is true, regardless of what the row says. get_site_url() does not: it passes the stored value through set_url_scheme() with whatever scheme the caller asked for.
So a site with http:// still in both rows, served over TLS, will emit correct front-end links and wrong asset URLs. The mixed-content warnings point at wp-content because siteurl is the row that is lying.
Do the whole sequence on a copy first
The failure mode here is not subtle breakage, it is being locked out of the install you need in order to fix the install.
The useful habit is to run the full sequence once somewhere disposable, in the order you will run it for real: update both rows, run search-replace, load the front end, load the admin, check one asset URL. A SandyWP sandbox built from your live site with the Cloner lands with URLs already rewritten, so you are rehearsing the change against real content and real serialized options rather than a clean install.
If you break it, delete it and make another. That is the whole point of doing it there, and it is the same reason upgrade testing belongs on a throwaway copy.
When changing these rows is the wrong answer
A few jobs look like a URL change and are not.
Moving a single page or post. That is the slug, edited on the post. Nothing to do with home.
Serving one site on two domains. Changing the rows does not do this. You want a redirect at the web server, or a genuine multisite install.
A temporary preview on a different hostname. Set the constants, do not write the rows. This is the one case where the override behaviour is exactly what you want, because you are not trying to make it permanent.
Keeping the old URLs working. Nothing in WordPress does this for you. A 301 from the old host is a web server or DNS job, and if you skip it the links you built over years stop resolving no matter how clean your wp_options table is.
