Tutorial

The Right HTTP Status Code for Maintenance Mode (and How to Set It in Nginx, Apache and WordPress)

Back to Blog
Managing servers the hard way? Panelica gives you isolated hosting, built-in Docker and AI-assisted management.
Start free

Maintenance mode returns 503 Service Unavailable with a Retry-After header. Not 200, which tells search engines your placeholder is the page's real content. Not 302, which can get the temporary redirect cached. Not 403, which suggests the visitor is forbidden rather than the site being briefly closed. 503 plus Retry-After is the only combination that both browsers and crawlers interpret as "come back shortly".

Why the wrong code costs more than the downtime

A two-hour maintenance window is invisible to your rankings if it is announced correctly. The damage comes from the announcement, not the window.

Serve the maintenance page with 200 OK and you have told every crawler that this URL's content is now a short apology. Do it across the whole site during a recrawl and you have replaced your index entries with duplicate thin pages. Recovery takes far longer than the maintenance did.

Serve it with 302 and you introduce a redirect that may be cached at intermediate layers, so some visitors keep landing on the maintenance page after you finished.

Serve it with 404 — which some naive implementations do by removing the document root — and you are actively telling crawlers to drop the URLs.

Serve 503 with Retry-After and the behaviour is the one you want: crawlers postpone, keep the existing index entry, and come back. Browsers do not cache it. Uptime monitors flag it as an outage, correctly, so your own alerting still works.

Nginx

server {
    listen 443 ssl;
    server_name example.com;
    root /var/www/example.com/public;

    # Toggle by creating or removing /var/www/example.com/maintenance.flag
    set $maintenance 0;
    if (-f /var/www/example.com/maintenance.flag) { set $maintenance 1; }

    # Let your own address through to test the real site
    if ($remote_addr = "203.0.113.10") { set $maintenance 0; }

    if ($maintenance = 1) {
        return 503;
    }

    error_page 503 @maintenance;
    location @maintenance {
        root /var/www/example.com/maintenance;
        rewrite ^ /index.html break;
        add_header Retry-After 3600 always;
    }
}

Three details that are easy to get wrong. The always on add_header is required — without it the header is dropped on error responses, which is exactly the case you are configuring. The flag file approach means enabling and disabling maintenance is touch and rm, with no config reload and therefore no risk of a syntax error taking the site down for real. And the IP exemption is what lets you verify the deployment before you let the world back in.

Apache

<IfModule mod_rewrite.c>
    RewriteEngine On
    RewriteCond %{DOCUMENT_ROOT}/maintenance.flag -f
    RewriteCond %{REMOTE_ADDR} !^203\.0\.113\.10$
    RewriteCond %{REQUEST_URI} !^/maintenance\.html$
    RewriteRule ^.*$ /maintenance.html [R=503,L]

    ErrorDocument 503 /maintenance.html
    Header always set Retry-After "3600"
</IfModule>

The third RewriteCond is the one people forget: without it the rule rewrites the maintenance page to itself and you get a redirect loop instead of a maintenance notice.

WordPress

WordPress creates a .maintenance file automatically during core and plugin updates, and its built-in handler does return 503. The problem is the ones people add themselves — a plugin or a snippet that renders a "coming soon" page through the normal template path, which returns 200.

If you are doing it in code, be explicit:

function site_maintenance_mode() {
    if (current_user_can('administrator') || wp_doing_cron()) {
        return;
    }
    header('Retry-After: 3600');
    wp_die(
        '<h1>Scheduled maintenance</h1><p>We will be back within the hour.</p>',
        'Scheduled maintenance',
        ['response' => 503]
    );
}
add_action('template_redirect', 'site_maintenance_mode');

The wp_doing_cron() check matters more than it looks: block cron and scheduled publishing, backups and any plugin relying on WP-Cron quietly stop for the duration.

If you use a maintenance-mode plugin, verify rather than trust. curl -sSI https://example.com/ during a test window either shows HTTP/2 503 or it does not, and a surprising number of popular plugins default to 200.

Choosing the Retry-After value

Retry-After takes either a number of seconds or an HTTP date. Both are valid; seconds are easier to get right.

Set it to a realistic estimate, and prefer slightly generous over optimistic. A crawler that returns after the interval and finds you still down learns nothing good, whereas returning early costs nothing. For an hour-long window, 3600. For an overnight migration, use a date. For something you genuinely cannot estimate, omit the header rather than inventing a number — 503 alone is still handled correctly, just without the scheduling hint.

The five-minute checklist before you flip the switch

  1. curl -sSI against your own site returns 503 and shows Retry-After.
  2. Your IP is exempted and you can see the real site.
  3. The maintenance page itself is not caught by the redirect rule.
  4. Static assets used by the maintenance page load — an unstyled page that also refuses its own CSS looks like a crash, not a notice.
  5. Turning it off requires no configuration reload.
  6. Your uptime monitor is silenced for the window, so a real failure during it is still distinguishable.

The last one is the one people skip and regret. Maintenance windows are exactly when something else breaks, and if every alert channel is muted you find out from a customer.

For the wider question of what each 5xx code signals — and why a 503 is a very different message from a 502 — see our breakdown of 500 vs 502 vs 503 vs 504.

Frequently asked questions

What HTTP status code should a maintenance page return?

503 Service Unavailable, with a Retry-After header. This tells crawlers the outage is temporary so index entries are preserved, and it prevents browsers and intermediate caches from storing the placeholder.

Is 503 bad for SEO?

Not for short, correctly signalled maintenance. Search engines are designed to postpone crawling on a 503 with Retry-After and retain existing rankings. Extended periods of 503 — days rather than hours — do eventually lead to pages being dropped.

Should I use 503 or 302 for maintenance mode?

503. A 302 redirect to a maintenance page can be cached by intermediaries, leaving visitors on the placeholder after maintenance ends, and it does not tell crawlers the situation is temporary in the way 503 does.

How do I let my own IP address through during maintenance?

Add a condition on $remote_addr in Nginx or %{REMOTE_ADDR} in Apache that skips the maintenance rule for your address. Behind a CDN, match on the forwarded client IP instead, since the direct address will be the CDN's.

Does WordPress maintenance mode return the correct status code?

The built-in .maintenance handler used during updates returns 503. Custom maintenance and coming-soon implementations frequently return 200 because they render through the normal template path. Verify with curl -sSI rather than assuming.

Security-first hosting panel

Hosting management, the modern way.

Panelica is a modern, security-first hosting panel — isolated services, built-in Docker and AI-assisted management, with one-click migration from any panel.

Zero-downtime migration Fully isolated services Cancel anytime
Share:
Skip the next emergency patch.