2026年11月12日
2026年11月12日
WordPress Maintenance Mode: .maintenance, 503 and an IP Allowlist
This article is a localized edition of the Japanese original. Read the Japanese original
While redesigning a site or running a large update, you may want visitors to see a maintenance notice while administrators can still check the site. WordPress has a built-in maintenance mode used during updates, but it switches itself off after 10 minutes, so it isn't suited to longer work.
This article first looks at how the built-in .maintenance file works, then shows how to implement a maintenance mode as an mu-plugin that you switch on and off with constants in wp-config.php. IP-based access is handled without trusting headers that can be spoofed. Everything here was checked against the WordPress 6.9 source code as of October 2026.
How the built-in maintenance mode (.maintenance) works
If a file named .maintenance exists in the WordPress root directory (the same place as wp-config.php), WordPress runs in maintenance mode. This is the short maintenance notice shown briefly while plugins or core are being updated (the English text is Briefly unavailable for scheduled maintenance. Check back in a minute.).
- Inside the file, the variable
$upgradingholds the start time (a UNIX timestamp). - Once 10 minutes or more have passed since
$upgrading, the site displays normally again even if the file is still there. - During maintenance, every page stops, including the admin screens. Administrators can't do anything either.
- The response has HTTP status 503 with
Retry-After: 600.
WP-CLI can create and remove this file with the following commands.
wp maintenance-mode activate # create .maintenance and enable it
wp maintenance-mode status # show the status
wp maintenance-mode is-active # exit code 0 when active (for scripts)
wp maintenance-mode deactivate # disable it
The file created by activate also contains its creation time, so it is treated as inactive after 10 minutes. If an update stops halfway and a leftover .maintenance keeps the site from loading, deleting this file brings the site back.
Changing the screen (wp-content/maintenance.php)
If you put wp-content/maintenance.php in place, this file is loaded instead of the default screen. However, it runs before WordPress functions are loaded, so WordPress functions such as bloginfo() and esc_html() are not available. You also have to send the status code yourself.
<?php
// wp-content/maintenance.php (WordPress functions are not available)
http_response_code( 503 );
header( 'Retry-After: 600' );
header( 'Content-Type: text/html; charset=UTF-8' );
?>
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Under maintenance</title>
</head>
<body>
<h1>Under maintenance</h1>
<p>We are currently updating the site. Please check back shortly.</p>
</body>
</html>
.maintenance is a mechanism for "stopping the site for the few minutes of an update". If the work takes longer, or administrators want to view the site while working, build a separate one as described next.
A maintenance mode toggled by constants in wp-config.php
Here we build a maintenance mode as an mu-plugin that behaves like this:
| Who / what | Behavior |
|---|---|
| Ordinary visitors | Get a 503 and a maintenance notice |
Logged-in administrators (manage_options capability) |
See the site as usual |
| Allowed IP addresses | See the site as usual, even when not logged in |
| Admin screens, login screen, WP-Cron, WP-CLI | Not blocked |
1. Add constants to wp-config.php
Add the following constants to wp-config.php above the line That's all, stop editing!. You may change the MYSITE_ prefix to your own so the names don't clash with other plugins.
// Enable maintenance mode (set to false or remove the line when done)
define( 'MYSITE_MAINTENANCE', true );
// IP addresses allowed through without logging in (comma-separated, optional)
define( 'MYSITE_MAINTENANCE_IPS', '203.0.113.10,2001:db8::10' );
// Optional time window. Include the time zone
define( 'MYSITE_MAINTENANCE_START', '2026-11-15 02:00:00 +09:00' );
define( 'MYSITE_MAINTENANCE_END', '2026-11-15 04:00:00 +09:00' );
With MYSITE_MAINTENANCE_START and MYSITE_MAINTENANCE_END set, the maintenance screen appears only between those times. Unlike switching on and off with WP-Cron, it switches on schedule regardless of whether the site gets traffic.
2. Add the mu-plugin
Put the following file, named mysite-maintenance.php, in the wp-content/mu-plugins/ folder (create it if it doesn't exist). Files in mu-plugins load automatically and can't be deactivated from the admin screen.
<?php
/**
* Plugin Name: Mysite Maintenance Mode
* Description: Toggle maintenance mode with the MYSITE_MAINTENANCE constant in wp-config.php
*/
add_action( 'wp_loaded', 'mysite_maintenance_mode' );
function mysite_maintenance_mode() {
if ( ! defined( 'MYSITE_MAINTENANCE' ) || ! MYSITE_MAINTENANCE ) {
return;
}
// If a time window is set, only enable within it
$now = time();
$start = defined( 'MYSITE_MAINTENANCE_START' ) ? strtotime( MYSITE_MAINTENANCE_START ) : false;
$end = defined( 'MYSITE_MAINTENANCE_END' ) ? strtotime( MYSITE_MAINTENANCE_END ) : false;
if ( ( $start && $now < $start ) || ( $end && $now >= $end ) ) {
return;
}
// Don't block admin screens (including admin-ajax.php), WP-Cron or WP-CLI
if ( is_admin() || wp_doing_cron() || ( defined( 'WP_CLI' ) && WP_CLI ) ) {
return;
}
// Don't block the login screen (otherwise administrators can't log in)
if ( isset( $GLOBALS['pagenow'] ) && 'wp-login.php' === $GLOBALS['pagenow'] ) {
return;
}
// Let logged-in administrators through
if ( current_user_can( 'manage_options' ) ) {
return;
}
// Let allowed IP addresses through. Only REMOTE_ADDR is used for the check
if ( defined( 'MYSITE_MAINTENANCE_IPS' ) && isset( $_SERVER['REMOTE_ADDR'] ) ) {
$allowed = array_filter( array_map( 'trim', explode( ',', MYSITE_MAINTENANCE_IPS ) ) );
if ( in_array( $_SERVER['REMOTE_ADDR'], $allowed, true ) ) {
return;
}
}
// If the end time is known, put the seconds until then in Retry-After
$retry_after = ( $end && $end > $now ) ? $end - $now : 3600;
status_header( 503 );
nocache_headers();
header( 'Retry-After: ' . $retry_after );
header( 'Content-Type: text/html; charset=UTF-8' );
// If the theme has maintenance-page.php, use it for the display
$template = get_stylesheet_directory() . '/maintenance-page.php';
if ( file_exists( $template ) ) {
include $template;
exit;
}
?>
<!DOCTYPE html>
<html <?php language_attributes(); ?>>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="robots" content="noindex">
<title><?php echo esc_html( 'Under maintenance | ' . get_bloginfo( 'name' ) ); ?></title>
</head>
<body>
<h1>Under maintenance</h1>
<p>We are currently updating the site. Please check back shortly.</p>
<?php if ( $end ) : ?>
<p>Expected end: <?php echo esc_html( wp_date( 'F j, Y H:i', $end ) ); ?></p>
<?php endif; ?>
</body>
</html>
<?php
exit;
}
Key points:
wp_loadedis an action, not a filter. It runs once WordPress, plugins and the theme have finished loading, socurrent_user_can()can check the logged-in user's capabilities.is_admin()is true for admin-screen requests. When a visitor who isn't logged in opens/wp-admin/, they are redirected to the login screen as usual.- If you use a plugin that changes the login URL, the
wp-login.phpcheck alone may not let you through. Before starting the work, confirm that you can log in while logged out. nocache_headers()keeps browsers and proxies from caching the maintenance screen.- To style the page, create
maintenance-page.phpin the child theme. At this point WordPress functions are available, so you can useget_bloginfo()andesc_html().
Check IP addresses with REMOTE_ADDR
When allowing access by IP address, don't read request headers such as X-Forwarded-For or CF-Connecting-IP. Whoever sends the request can set these headers freely. For example, a request like the one below can impersonate an allowed IP address against code that trusts the header as-is.
curl -H "X-Forwarded-For: 203.0.113.10" https://example.com/
$_SERVER['REMOTE_ADDR'] is the address of the party actually connected to the web server, so a header can't rewrite it.
If the site sits behind a CDN such as Cloudflare or a load balancer, REMOTE_ADDR is the CDN's address. In that case, don't read the header in PHP; use the web server's own feature to replace it with the original IP address only when the connection comes from a trusted proxy.
- Nginx:
set_real_ip_from(the address ranges of trusted proxies) andreal_ip_headerinngx_http_realip_module - Apache 2.4:
RemoteIPTrustedProxyandRemoteIPHeaderinmod_remoteip
PHP then receives the replaced REMOTE_ADDR, so the mu-plugin above works unchanged. The concrete setup for Cloudflare is explained in the related Cloudflare WAF article.
The same IPv6 address can be written differently depending on how it is abbreviated. Write it in MYSITE_MAINTENANCE_IPS exactly as it appears in your access log.
Check that it works
While logged out (or in a private browser window), check the status code and headers.
curl -sI https://example.com/ | grep -iE "^HTTP|retry-after|cache-control"
# HTTP/2 503, retry-after, and cache-control: no-cache, must-revalidate, max-age=0, ... mean it is active
Also check the following:
- Logged in as an administrator, you see the normal pages.
https://example.com/wp-login.phpopens and you can log in.- From an allowed IP address, you see the normal pages without logging in.
Effects on caching and search engines
- Page cache: caching plugins, server caches and CDN caches can serve old pages before the request reaches WordPress. Clear each of them before and after switching maintenance mode.
- Why return 503: if the maintenance screen is returned with 200, search engines may treat the maintenance notice as the page's content. When Google receives a 5xx status, it ignores the content and temporarily slows crawling.
- Long maintenance: Google's documentation says URLs that keep returning server errors are eventually dropped from the index. Use this for work that ends within hours; if you need to stop the site for longer, consider ways to keep at least the main pages available.
When maintenance is over
- Set
MYSITE_MAINTENANCEinwp-config.phptofalse(or remove the line). - Clear the page cache and the CDN cache.
- While logged out, confirm that the status is back to 200.
The mu-plugin does nothing without the constant, so you can leave the file in place for next time.
Summary
- The built-in
.maintenanceswitches off 10 minutes after it is created and also stops the admin screens. It is meant for the few minutes of an update. - WordPress functions are not available in
wp-content/maintenance.php, so send the 503 yourself. - For longer work, use an mu-plugin driven by constants in
wp-config.phpand thewp_loadedaction, letting admin screens, the login screen and administrators through. - Check IP addresses with
REMOTE_ADDR; behind a CDN, replace it with the original IP address using the web server's realip / remoteip settings. - Return 503 and
Retry-After, and clear caches before and after switching.
Related articles:
Questions about WordPress? The publisher of this site is based in Japan and replies in Japanese. Contact (Japanese)