SiteError.comYour friendly guide to HTTP status codes
Status CodesBlog
  1. Home
  2. Blog
  3. Understanding HTTP 503 Service Unavailable: Planned Downtime, Overload, and Graceful Failure

Understanding HTTP 503 Service Unavailable: Planned Downtime, Overload, and Graceful Failure

May 12, 202610 min read
5xxServer Error

There's a meaningful difference between "my server is broken" and "my server is busy." 500 is the first one. 503 is the second. Treating them as interchangeable is one of the most common mistakes in production systems — it confuses load balancers, misleads on-call engineers, and quietly hurts your search rankings during deploys. Let's unpack what 503 actually means, why the Retry-After header matters more than the status code itself, and how to use 503 to fail gracefully instead of catastrophically.

What Is a 503?

A 503 means the server understood the request and is otherwise healthy — it's just temporarily unable to handle it right now.

The 503 (Service Unavailable) status code indicates that the server is currently unable to handle the request due to a temporary overload or scheduled maintenance, which will likely be alleviated after some delay. The server MAY send a Retry-After header field to suggest an appropriate amount of time for the client to wait before retrying the request. — RFC 9110, Section 15.6.4

In plain English: nothing is broken, the situation is temporary, and you should come back later. The "later" part is doing real work — 503 isn't a dead-end like a 500. It's an invitation to retry.

The critical word in the RFC is temporary. If the unavailability is permanent, you don't want 503 — you want 410. If your code is broken, you don't want 503 — you want 500. 503 is reserved for the specific case where the service is fine and the client should try again.

Why Retry-After Is the Real Story

503 without Retry-After is half a response. With it, you turn a refusal into a conversation:

HTTP/1.1 503 Service Unavailable
Retry-After: 120
Content-Type: text/html
 
<h1>We'll be right back</h1>

Two valid forms of Retry-After:

  • Seconds: Retry-After: 120 — try again in two minutes.
  • HTTP-date: Retry-After: Wed, 13 May 2026 09:00:00 GMT — try again after this timestamp.

Why this matters in practice:

  • Googlebot honors it. Without Retry-After, repeated 503s can knock pages out of the index. With it, Google holds off, recrawls after the suggested delay, and treats the downtime as planned. Google's own guidance is explicit: return 503 with Retry-After for maintenance.
  • Polite clients honor it. Well-behaved SDKs and CLIs respect Retry-After and back off. Without it, they hammer you the second you come back up.
  • Load balancers use it. Some health-check systems treat 503 + Retry-After as "drain this instance for N seconds" rather than "this instance is broken."

A 503 without Retry-After is technically valid, but it's like leaving a "back in 5" sign that doesn't say when the 5 minutes started.

When to Return 503

The five canonical scenarios:

  1. Planned maintenance. Deploys, database migrations, infrastructure swaps. Put the app behind a 503 page during the cutover; remove it the second traffic-serving resumes.
  2. Database connection pool exhausted. You can't serve the request right now without waiting an unbounded amount of time. Failing fast with 503 is better than holding sockets open.
  3. Critical dependency outage. An auth provider, payments API, or other hard dependency is down. You could return 500, but 503 communicates "this is a temporary external issue, not a bug in our code."
  4. Load shedding. Your queue depth is past the threshold where new requests will time out anyway. Reject early with 503 + Retry-After so clients back off and the queue drains.
  5. Graceful degradation during scale-up. A traffic spike is in progress and your autoscaler hasn't caught up yet. Better to return 503 to 5% of users for two minutes than to brown out for everyone.

If the answer to "what's wrong?" is "the code crashed," you want 500. If the answer is "everything is fine, we're just busy or paused," you want 503.

503 vs 500 vs 502 vs 504 vs 429

The 5xx family is famously easy to misuse. A precise table:

StatusMeansWho is at faultShould the client retry?
500 Internal Server Error"Something in my code went wrong."The server is brokenMaybe — with backoff and limited attempts
502 Bad Gateway"I'm a proxy and upstream gave me garbage."The upstream is brokenYes — likely transient
503 Service Unavailable"I'm healthy but temporarily refusing work."Nobody is brokenYes — honor Retry-After
504 Gateway Timeout"I'm a proxy and upstream went silent."The upstream is slow or downYes — likely transient
429 Too Many Requests"You specifically are sending too much."The client is misbehavingYes — honor Retry-After, slow down

The single most useful distinction: 503 says the service is fine; 500 says it isn't. Operators reading dashboards make different decisions based on which one is firing. A wall of 500s means "wake someone up." A wall of 503s with Retry-After means "the scheduled deploy is in progress." Don't blur that signal.

429 deserves a special call-out because it overlaps with 503 in load-shedding scenarios. Use 503 when the whole service is busy; use 429 when one specific client is the problem. Our understanding 429 too many requests post goes deep on the per-client case.

Returning 503 Correctly Across Platforms

Express / Node.js

app.use(async (req, res, next) => {
  if (isInMaintenanceMode()) {
    res.set("Retry-After", "120");
    res.set("Cache-Control", "no-store");
    return res.status(503).send("Maintenance in progress — back in a few minutes.");
  }
  next();
});

Two non-obvious bits: Cache-Control: no-store keeps your CDN from caching the maintenance page across users, and registering this as middleware (not a route) ensures every path returns 503 during the window.

Next.js Route Handler

// app/api/orders/route.ts
import { NextResponse } from "next/server";
 
export async function POST(request: Request) {
  if (await isDbPoolExhausted()) {
    return new NextResponse(
      JSON.stringify({ error: "service_unavailable" }),
      {
        status: 503,
        headers: {
          "Retry-After": "30",
          "Content-Type": "application/json",
          "Cache-Control": "no-store",
        },
      },
    );
  }
  // ... real work
}

NGINX (maintenance mode)

A pattern that's been deployed a million times — drop a flag file to flip the whole site into 503:

server {
  set $maintenance 0;
  if (-f /etc/nginx/maintenance.flag) {
    set $maintenance 1;
  }
  if ($maintenance) {
    return 503;
  }
 
  error_page 503 @maintenance;
  location @maintenance {
    root /var/www/static;
    try_files /503.html =503;
    add_header Retry-After 60 always;
    add_header Cache-Control "no-store" always;
  }
 
  location / {
    proxy_pass http://app_backend;
  }
}

touch /etc/nginx/maintenance.flag enters maintenance; rm exits. No reload required if you keep the if block tidy.

Load shedding in code

const QUEUE_THRESHOLD = 500;
 
app.use((req, res, next) => {
  if (jobQueue.depth > QUEUE_THRESHOLD) {
    res.set("Retry-After", "5");
    return res.status(503).json({ error: "overloaded" });
  }
  next();
});

Shedding load early is almost always better than letting the queue grow. A request that would have timed out anyway might as well fail fast and tell the client when to come back.

Maintenance Windows Without Trashing Your SEO

503 is the correct code to return during a deploy or maintenance. Google's own webmaster guidance is explicit about this, and getting it wrong has real consequences:

  • Don't return 200 with "we'll be back." That's a soft success — Google will index your maintenance copy as the page content. We covered the soft-error pattern in our understanding 404 not found post.
  • Don't return 302 to a "/maintenance" URL. That tells crawlers the content has temporarily moved, which is the wrong signal entirely.
  • Don't return 500. Persistent 500s tell crawlers your site is broken, not that you're doing planned work.
  • Always include Retry-After. This is the difference between Google waiting patiently and Google quietly demoting your pages.
  • Keep the window short. A few hours of 503s is invisible to search rankings. A few days of 503s is not.
  • Serve a real HTML page, not an empty 503. Users hitting the site during the window should see your brand, not the default nginx error.

Health Checks and 503

503 is also the right answer for readiness checks. A common architecture:

app.get("/health/ready", async (req, res) => {
  try {
    await Promise.all([
      db.query("SELECT 1"),
      cache.ping(),
    ]);
    res.json({ status: "ok" });
  } catch (err) {
    res.set("Retry-After", "5");
    res.status(503).json({ status: "not_ready", reason: String(err) });
  }
});

A load balancer hitting this endpoint reads 503 and pulls the instance out of rotation until it's healthy again. The instance itself stays up; traffic just stops being routed to it.

The distinction worth knowing:

  • Liveness check — "is the process alive?" If this fails, restart the container. Should almost never return 503; if the process is broken, return 500 or stop responding entirely.
  • Readiness check — "should this instance receive traffic right now?" 503 is appropriate when a dependency is down but the process is fine and may recover without a restart.

Confusing the two leads to restart loops (liveness check returns 503 → orchestrator kills the container → next instance hits the same downstream → loop). Pick the right semantic.

Common Pitfalls

  1. No Retry-After header. Clients can't back off intelligently and Googlebot doesn't know when to recheck. The single most impactful fix is adding this header.
  2. Caching the 503 response. A CDN with Cache-Control: public on a 503 page can serve maintenance to all users for hours after the deploy finishes. Always set Cache-Control: no-store on 503 responses.
  3. Returning 503 forever. If a URL is gone for good, 503 is the wrong code — return 410 and let crawlers drop it.
  4. Using 500 for overload. A wall of 500s wakes someone up at 3am. A wall of 503s with Retry-After does not. Reserve 500 for "we are actually broken."
  5. Hard-coding 503 instead of using a circuit breaker. If you're returning 503 because a downstream dependency is flaky, a proper circuit breaker (with half-open probing) is far better than a hand-rolled "is the API up?" check. Our understanding 502 errors post discusses circuit breakers in the proxy context.
  6. Forgetting that liveness and readiness need different semantics. A liveness probe returning 503 will get your pod killed in many orchestrators. Use 200/non-200 for liveness; reserve 503 for readiness.
  7. Maintenance mode that doesn't apply to API routes. A common bug: the HTML routes return a nice 503 page but /api/* still hits a partially-migrated backend. Apply the maintenance check at the edge, not per-route.

Wrapping Up

503 is one of the most useful status codes in the 5xx range, and one of the most misused. The rules of thumb:

  • Use 503 for temporary refusals — maintenance, overload, dependency outage
  • Always include Retry-After (seconds or HTTP-date) — this is what makes the response actionable
  • Set Cache-Control: no-store so CDNs don't cache your maintenance page
  • Return 500 for "we're broken," 503 for "we're busy or paused"
  • For readiness checks, 503 is correct; for liveness, it isn't
  • Keep maintenance windows short — long 503 stretches hurt SEO
  • Don't blanket-redirect to /maintenance (use 503 directly) and don't return 200 with apology copy

For more, see our pages on 503 Service Unavailable, 500 Internal Server Error, 502 Bad Gateway, 504 Gateway Timeout, and 429 Too Many Requests. If you're shipping rate-limiting work, the understanding 429 too many requests post covers the per-client case. For the upstream-proxy story, the understanding 502 errors post pairs naturally with this one. And for the "what is actually broken?" sibling, the understanding 500 errors post has you covered.

Related Status Codes

💥500Internal Server Error🚧501Not Implemented🌉502Bad Gateway🔧503Service Unavailable⌛504Gateway Timeout📡505HTTP Version Not Supported🔄506Variant Also Negotiates💾507Insufficient Storage🔁508Loop Detected🧩510Not Extended📶511Network Authentication Required
Back to Blog

Popular Status Codes

  • 200 OK
  • 301 Moved Permanently
  • 302 Found
  • 400 Bad Request
  • 401 Unauthorized
  • 403 Forbidden
  • 404 Not Found
  • 500 Internal Server Error
  • 502 Bad Gateway
  • 503 Service Unavailable

Compare Codes

  • 401 vs 403
  • 301 vs 302
  • 404 vs 410
  • 500 vs 502
  • Compare any codes →

Categories

  • Informational
  • Success
  • Redirection
  • Client Error
  • Server Error
  • NGINX
  • Cloudflare
  • AWS ELB
  • Microsoft IIS

Tools

  • Cheat Sheet
  • Status Code Quiz
  • URL Checker
  • API Playground
  • Blog

© 2026 SiteError.com. All rights reserved.