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 $upgrading holds 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_loaded is an action, not a filter. It runs once WordPress, plugins and the theme have finished loading, so current_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.php check 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.php in the child theme. At this point WordPress functions are available, so you can use get_bloginfo() and esc_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) and real_ip_header in ngx_http_realip_module
  • Apache 2.4: RemoteIPTrustedProxy and RemoteIPHeader in mod_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:

  1. Logged in as an administrator, you see the normal pages.
  2. https://example.com/wp-login.php opens and you can log in.
  3. 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

  1. Set MYSITE_MAINTENANCE in wp-config.php to false (or remove the line).
  2. Clear the page cache and the CDN cache.
  3. 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 .maintenance switches 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.php and the wp_loaded action, 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)

お気軽にご相談ください

お見積りへ お問い合わせへ
▲