HTTP status code · Server errors (5xx)
503 Service Unavailable
503 Service Unavailable means the server is temporarily unable to handle the request because it is overloaded or down for maintenance, and it expects to recover. The most common causes are a traffic spike that exhausts the server's workers and a deploy or maintenance window during which the app is switched off.
| Class | 5xx, Server errors |
|---|---|
| Defined in | RFC 9110 §15.6.4 |
| Cacheable by default | Only with explicit Cache-Control or Expires; caches store it only with explicit freshness; a stored maintenance page can outlive the maintenance |
| Safe to retry | Yes, after the delay in Retry-After if present, otherwise with exponential backoff |
| Relevant headers |
|
What 503 means
RFC 9110, section 15.6.4, describes 503 as the server being unable to handle the request due to temporary overload or scheduled maintenance, which will likely be alleviated after some delay. The word that matters is temporary. The server may add a Retry-After header, either a number of seconds or an HTTP date, to say when to come back.
Unlike 502 and 504, a 503 is often a decision, not an accident. Load balancers return it when no healthy target is registered, Go's http.TimeoutHandler returns it when a handler runs past its deadline, and nginx's limit_req module rejects excess requests with 503 unless you set limit_req_status to 429.
For search engines, 503 with Retry-After is the correct way to take a site offline for maintenance. Google treats a short 503 as a signal to come back later rather than to drop the pages, which a 200 maintenance page or a 404 would not achieve.
Common causes
If you are visiting the site
- The site is in a planned maintenance window or in the middle of a deploy.
- A surge of visitors, such as a ticket sale, a launch or a viral link, has used up the site's capacity.
- Your requests tripped the site's rate limiter, if it is configured to answer 503 instead of 429.
If you run the server
- All workers are busy: PHP-FPM hit pm.max_children, a Gunicorn or Puma pool is full, or a database connection pool is exhausted, so new requests are turned away.
- The load balancer has no healthy targets: every instance fails the health check, or none are registered after a deploy (an AWS ALB answers 503 in that case).
- Maintenance mode is on, for example a flag file your nginx config checks, or a platform switch such as a maintenance page on your host.
- A timeout wrapper cut the request short: Go's http.TimeoutHandler writes a 503 when the wrapped handler exceeds its duration.
- nginx limit_req or limit_conn rejecting bursts with its default 503 status.
How to fix it
If you are visiting the site
- Wait and try again; if the page shows a time or the response has Retry-After, wait at least that long.
- Check the site's status page or social accounts for a maintenance notice.
- Avoid hammering reload during a sale or launch; each retry adds to the load that caused the 503.
If you run the server
- Find which layer sent it (Server header, load balancer logs) before scaling anything: a balancer with zero healthy targets needs a fixed health check, not more instances.
- If workers are saturated, look at why requests are slow (slow queries, external calls without timeouts) before raising pm.max_children or pool sizes, which can just move the bottleneck to the database.
- Add capacity or autoscaling for real traffic peaks, and put a queue in front of expensive work so it does not hold request workers.
- For maintenance, return 503 with Retry-After and Cache-Control: no-store, and keep it short; many hours of 503 start to cost search visibility.
- If the 503 is really rate limiting, switch it to 429 so clients can tell "you are too fast" apart from "we are down".
How to send 503
app.post('/exports', (req, res) => {
res.set('Retry-After', '120');
res.status(503).json({ error: 'Export queue is full, try again in 2 minutes' });
});// app/exports/route.ts
export async function POST() {
return Response.json(
{ error: 'Export queue is full, try again in 2 minutes' },
{ status: 503, headers: { 'Retry-After': '120' } }
);
}mux.HandleFunc("POST /exports", func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Retry-After", "120")
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusServiceUnavailable) // 503
w.Write([]byte(`{"error":"Export queue is full, try again in 2 minutes"}`))
})from fastapi import FastAPI, HTTPException
app = FastAPI()
@app.post("/exports")
def create_export():
raise HTTPException(status_code=503, detail="Export queue is full, try again in 2 minutes", headers={"Retry-After": "120"})# Maintenance mode: touch /var/www/maintenance.on to enable
server {
location / {
if (-f /var/www/maintenance.on) {
return 503;
}
proxy_pass http://app;
}
error_page 503 @maintenance;
location @maintenance {
add_header Retry-After 600 always;
add_header Cache-Control no-store always;
root /var/www/errors;
rewrite ^ /maintenance.html break;
}
}Commonly confused with
- 503 vs 429
- 429 says this client is sending too many requests; 503 says the server is struggling for everyone. Rate limiters should use 429, even though nginx defaults to 503.
- 503 vs 502
- A 502 is a proxy reporting a broken upstream response; a 503 is usually a deliberate refusal to take more work right now.
- 503 vs 500
- A 500 is an unexpected failure with no promise of recovery; a 503 explicitly says the condition is temporary.
Frequently asked questions
- How long does a 503 Service Unavailable error last?
- It depends on the cause. A deploy or restart clears in seconds to minutes, planned maintenance lasts as long as the window announced, and overload lasts until traffic drops or capacity is added. A Retry-After header, when present, is the server's own estimate.
- Is 503 good for SEO during maintenance?
- Yes, it is the recommended status. Googlebot treats a temporary 503 as a reason to retry later instead of removing the pages. Keep the outage short: if a URL returns 503 for days, Google starts treating it as gone.
- Should rate limiting return 503 or 429?
- Use 429 Too Many Requests, defined in RFC 6585 for exactly this purpose, and include Retry-After. nginx limit_req uses 503 by default for historical reasons; set limit_req_status 429 to change it.
- Why does my load balancer return 503?
- Usually because it has no healthy backend to send the request to: all targets fail the health check, the health check path returns an error, or no targets are registered. Fix the health check or the instances, not the balancer.
- What does "503 Service Temporarily Unavailable" mean?
- It is the same 503 status with a different reason phrase. Apache and some other servers use "Service Temporarily Unavailable" in their default error pages; clients only look at the number.
Last reviewed by Arielton Oberek.