A maintenance page should be a 503 response with a Retry-After header, a status-page link, a contact path, and an inline way for the user to come back without a refresh. Most maintenance pages skip all of that and ship a static “we’ll be right back” that hurts SEO, hurts trust, and gives the user nothing to do but hit reload. The interesting thing about a maintenance page is that the engineering is small (a 503 status code, a header, a route) and the user-experience choices are large (what to show, what to hide, what to link, how to update).
This post is the version that ships. The first half is the engineering: the response code, the headers, the routing, the deployment. The second half is the UX: what to show, what to ask the user to do, what the page should not be.
The reason the maintenance-page question keeps coming up is that there is no universal “the right maintenance page” — there are several, and they map to different scenarios. A planned deploy is different from an unplanned outage. A 30-second deploy is different from a 30-minute database migration. A user with a form half-filled is different from a user landing from a search result. The page has to handle all of them, and the engineering is the easy part.
Table of contents
- The direct answer
- The 503 response: the status code that matters
- The Retry-After header: the part most pages skip
- The routing: where the maintenance page lives
- The deploy-time toggle: how to flip the switch without a deploy
- The UX: what the page should actually say
- The SEO reality: do not block Google forever
- The opinion this post is built on
- FAQ
The direct answer
A maintenance page is:
HTTP/1.1 503 Service Unavailable
Content-Type: text/html; charset=utf-8
Retry-After: 300
Cache-Control: no-store
<!doctype html>
<html>
<head>
<title>We'll be right back — RunxBuild</title>
</head>
<body>
<main>
<h1>Briefly down for maintenance</h1>
<p>We're shipping an update. Try again in 5 minutes.</p>
<p><a href="https://status.example.com">Live status</a> · <a href="mailto:[email protected]">Get in touch</a></p>
</main>
</body>
</html>
The 503 says “we are temporarily unavailable, this is not a permanent failure.” The Retry-After says “come back in 5 minutes.” The body says “here is what is happening, here is where to find out more.” That is the entire contract.
The rest of the post is the engineering behind each piece and the UX choices that turn the contract into a page the user does not hate.
The 503 response: the status code that matters
The wrong status code is 200. The wrong status code is 404. The wrong status code is “the page is up but the navigation is broken.” The right status code is 503 Service Unavailable, with a Retry-After header indicating when to come back.
Why 503:
- Google’s crawlers know what 503 means. A 503 says “the server is temporarily unable to handle the request.” Google stops crawling, retries after the
Retry-Afterwindow, and resumes when the server is back. A 200 with a “we are down” body confuses the crawler. A 404 makes the page disappear from the index. Only 503 is “temporarily down, please come back.” - HTTP clients know what 503 means. Load balancers, monitoring, uptime checkers, and backoff libraries all handle 503 with the right behavior — back off, retry later, do not alarm. A 200 with a “down” body bypasses every one of those.
- Users know what 503 means (or do not, but the browser does). A modern browser shows a friendly “this site is down” page when the server returns 503. The user sees the same experience as if the site was completely offline, which is the honest answer.
The implementation is one line in the response: status 503. The discipline is to use 503 instead of 200 for every maintenance-mode response, including the ones that look “working” because the page is rendered.
The Retry-After header: the part most pages skip
Retry-After is the header that tells the client (browser, crawler, monitoring) when to come back. The value is either an HTTP date or a number of seconds.
Retry-After: 300
The right value depends on the situation:
- A 30-second deploy:
Retry-After: 30. Clients come back in 30 seconds, the deploy is done, the page works. - A 5-minute database migration:
Retry-After: 300. Clients come back in 5 minutes, the migration is done, the page works. - An unplanned outage:
Retry-After: 60and update the header as the incident progresses. The client retries every minute, and the response says “try again in 1 minute” until the incident is resolved.
The Retry-After header is what turns “users are hammering refresh” into “users are waiting politely.” Without it, the user is the retry mechanism. With it, the browser, the crawler, the monitoring — all of them — are the retry mechanism, and they are polite.
The implementation: one header in the response. The discipline: update the value as the situation evolves. The first response of the incident can have Retry-After: 60, the next can have Retry-After: 300 once the team has a better ETA, the last can drop the Retry-After once the service is back.
The routing: where the maintenance page lives
There are three reasonable places, in order of preference:
-
A separate static file at
/maintenance.html. The deploy platform’s reverse proxy serves this file directly, no application code is involved. The platform toggles between the application and the static file based on a flag. The application is fully drained, no traffic, no work. -
A route in the application that returns 503. The application is still running, but every request to the application is intercepted by a middleware that returns 503 with the maintenance page. The application is still consuming resources, but the user-facing experience is correct.
-
A custom error page in the reverse proxy or CDN. The CDN intercepts requests to the application and returns a 503 with the maintenance page. The application is bypassed entirely.
The first option is the cleanest. The application is offline, the static file is being served, and the platform is doing the work. The second is the fallback when the platform does not support static-file failover. The third is the right answer for a CDN-fronted application where the CDN can do the maintenance page faster than the application can.
The implementation: depends on the platform. The contract is the same — a 503 response with a Retry-After header and a sensible body. The mechanism is platform-specific.
The deploy-time toggle: how to flip the switch without a deploy
The trap is to flip the maintenance switch by deploying a new version of the application. The deploy is the most dangerous time to be making changes to the application, because the platform is already busy, and a second change in the middle of the first is the kind of mistake that ends in a postmortem.
The right answer is a platform-level toggle: a switch in the dashboard, an environment variable, a feature flag, a config file that the platform reads at request time. The flip is one click in the dashboard, no deploy, no rebuild, no restart. The platform serves the maintenance page for the next 30 seconds while the deploy is in progress, then serves the new application when the deploy is done.
A platform with a maintenance-mode toggle makes the maintenance story one click. A platform without one makes it a deploy, and the deploy is the worst time to be making changes.
The UX: what the page should actually say
The wrong maintenance page: a sad face, a “we are down” message, and a refresh button. The right maintenance page:
- Acknowledges the problem. “We are doing a planned update” or “We are recovering from an incident” — whichever is true. The user can read the message and understand what is happening.
- Gives an ETA or a status link. “Try again in 5 minutes” or “Live status at status.example.com.” The user knows when to come back or where to look for more information.
- Provides a contact path. A support email or a link to a contact form. The user has somewhere to go if the outage is urgent for them.
- Matches the brand. The same colors, fonts, and tone as the rest of the site. The user knows they are on the right site, not a phishing page.
The page should not:
- Show a stack trace or a server error message. The user does not need it, and the information is a security risk.
- Show a “we are down” message with no context. The user does not know what to do.
- Block the user from contacting support. The user has a question, the question needs an answer, the answer is in the support form.
- Pretend the site is working. The user is not fooled, and the deception is worse than the outage.
The right tone: matter-of-fact. “We are doing an update. Try again in 5 minutes. The status page has the details.” That is the message. Anything more is filler.
The SEO reality: do not block Google forever
A 503 with Retry-After is the right answer for a temporary outage. Google crawls back after the Retry-After window, sees the new state, and resumes. The SEO impact is minimal.
The trap is to leave the 503 in place. A site that returns 503 for more than a few days will start losing search rankings. Google’s crawler interprets “this site has been down for a week” as “this site is not coming back,” and the rankings decay.
For a planned multi-day outage (a major migration, a data center move), the right answer is to keep the application running in a read-only state and serve a 200 with a banner that says “we are doing maintenance.” The 200 keeps the SEO intact; the banner tells the user what is happening. The 503 is for sub-day outages, not for multi-day ones.
For an unplanned multi-day outage (a major incident, a data loss), the right answer is to put up a 200 with a single message and a contact path. The 503 is wrong here. The 200 with a useful message keeps the SEO and gives the user something to do.
The discipline: the 503 is a tool, not a default. Use it for sub-day outages. Use 200 with a banner for longer ones. The SEO follows.
The opinion this post is built on
The maintenance page is the one screen on a site that almost no one designs until they need it. The team writes the page at 2am during an incident, ships the apology, goes back to bed, and forgets about it until the next incident. The next incident ships a different apology. The page is never tested. The SEO is never checked. The UX is never considered.
The right answer is to design the maintenance page before the incident, ship it as part of the standard deploy, and have a one-click toggle to activate it. The page is a feature, not a crisis response. The toggle is a platform feature, not a deploy. The team has the page ready, the platform has the toggle, and the next incident is a 30-second response instead of a 30-minute scramble.
The platform is the multiplier. A platform with a maintenance toggle, a static-file failover, a 503 with Retry-After, and a status page is a platform where the maintenance story is solved. A platform without those features forces the team to build them by hand, and the team will build them wrong, and the wrong version will ship in the next incident.
The page itself is small. The discipline is large. The next incident is the time the team is most likely to ship a bad version of the page, which is exactly when the page matters most. Build the right page before the incident. Use a platform that has the toggle. Test the page in staging. The next incident is the test, and the team will know whether the page works in the first 30 seconds of the response.
How this fits the rest of the stack
A maintenance page that the team keeps turned on too long is also a cost — the customers who bounce are the customers the team never converts, and the brand damage is harder to model than the platform bill. The team should know what the maintenance window actually costs in lost revenue and lost trust, and the platform bill should make the comparison clear. The RunxBuild hosting calculator is the right place to model that — pick the runtime size, the bandwidth, the database, and the traffic, and the calculator shows what the maintenance window costs in platform dollars alongside the customer-facing cost the team is already paying.
Useful related references:
FAQ
What HTTP status code should a maintenance page return?
503 Service Unavailable with a Retry-After header. 503 tells Google and HTTP clients that the service is temporarily down, not permanently failed. The Retry-After header tells them when to come back. Avoid 200 (which confuses crawlers), 404 (which makes the page disappear from the index), and 302 (which is a redirect, not a downtime signal).
How long should the Retry-After header be?
Depends on the outage. A 30-second deploy: Retry-After: 30. A 5-minute database migration: Retry-After: 300. An unplanned outage: start with Retry-After: 60 and update as the incident progresses. The Retry-After is what turns “users hammering refresh” into “users waiting politely.”
Should a maintenance page block Google?
A 503 with Retry-After is correct for sub-day outages. Google crawls back after the Retry-After window, sees the new state, and resumes. The SEO impact is minimal. For multi-day outages, the right answer is a 200 with a banner that says “we are doing maintenance” — the 200 keeps the SEO intact, the banner tells the user what is happening.
What should a maintenance page say?
Acknowledge the problem, give an ETA or a status link, provide a contact path, and match the brand. The right tone is matter-of-fact: “We are doing an update. Try again in 5 minutes. The status page has the details.” Avoid stack traces, avoid “we are down” with no context, avoid pretending the site is working.
Where should the maintenance page live?
The cleanest answer is a separate static file at /maintenance.html that the platform’s reverse proxy serves directly. The application is fully drained, no traffic, no work. The next-best answer is a route in the application that returns 503, with a middleware that intercepts every request. The third answer is a custom error page in the CDN, for CDN-fronted applications.
How do I activate the maintenance page without a deploy?
A platform-level toggle: a switch in the dashboard, an environment variable, a feature flag, a config file. The flip is one click, no deploy, no rebuild, no restart. The platform serves the maintenance page while the deploy is in progress, then serves the new application when the deploy is done. Avoid making the maintenance flip a deploy — the deploy is the worst time to be making changes.