There is exactly one reason to keep a staging environment: to see the result before you deploy it. If staging differs from production, that preview is a preview of a different site. Either it passes and production breaks, or it breaks only on staging and you spend hours chasing a problem that does not exist.
“Completely identical” is usually impossible and rarely necessary. What you actually need to decide is which differences change the outcome.
What must match, and what must not
Each item on the right fails in its own way. A different PHP version changes deprecation notices and type handling, so code that was quiet on staging starts complaining in production. A missing extension sends the feature down an entirely different code path. Database version and sql_mode are the quietest of all: an INSERT rejected under strict mode passes happily under a looser one.
Caching layers lie in both directions. If only production has an object cache, an N+1 query pattern you caught on staging disappears in production. If only production has a page cache, the “logged-in visitors are served a cached page” bug appears exclusively in production.
Comparing takes a handful of commands
Print the same facts on both sides and diff them. The habit turns “is it the environment?” from a suspicion into a checked fact.
wp --info
wp db query 'SELECT VERSION();'
wp db query 'SELECT @@sql_mode;'
wp eval 'echo implode( PHP_EOL, get_loaded_extensions() );' | sort
wp plugin list --status=active --field=name | sort
wp eval 'echo ini_get( "memory_limit" ), " ", ini_get( "max_execution_time" );'
Keeping staging from causing an incident
Staging holds a copy of real data, so left alone it will email real customers and get itself indexed. Both must be prevented in code. Relying on someone remembering to switch them off means one day nobody does.
<?php
// wp-content/mu-plugins/staging-guard.php
if ( 'production' !== wp_get_environment_type() ) {
// short-circuit delivery and report success (WP 5.7+)
add_filter( 'pre_wp_mail', '__return_true' );
// force it at read time, not in the option — survives a fresh DB import
add_filter( 'pre_option_blog_public', '__return_zero' );
}
The second filter matters because it intercepts the read rather than editing the option. Re-import the production database and the value does not come back to life. Add HTTP basic auth or an IP restriction on top and the staging URL has no route into search at all.
Data volume is part of the environment
One axis gets forgotten more than any other: a staging site with twenty posts cannot reproduce a problem that only appears at two hundred thousand. Archive queries, indexes and sitemap generation all scale with content. Pulling a sanitised copy of production data down, using the procedure from part four, is the only practical way to match this axis.
How the measuring and verification actually run is published step by step on our process page, and the performance background sits in the Performance archive.
Next part
Once staging says yes, it is time to move the code. The next part covers three deploy strategies — rsync, git on the server, and shipping a built artefact — and what separates them is not speed but the rollback story.