1. Library
  2. Computer Networks
  3. Http and the Web
  4. Status Codes

Updated 9 hours ago

The 503 Service Unavailable error is one of the few HTTP status codes that's genuinely optimistic. It doesn't say "something went wrong." It says "something is temporarily wrong, I know about it, and here's when to try again."

That's a fundamentally different relationship with failure.

The Promise of 503

503 is a contract between server and client:

  1. The server received and understood your request
  2. It cannot handle it right now
  3. This is temporary
  4. Here's when to come back
HTTP/1.1 503 Service Unavailable
Retry-After: 3600
Content-Type: application/json

{
    "error": "Service Unavailable",
    "message": "We're performing scheduled maintenance. Service resumes in 1 hour.",
    "retryAfter": 3600
}

The Retry-After header is the promise made concrete. Without it, 503 is just a complaint. With it, 503 is a plan.

When Servers Say "Not Right Now"

Planned Maintenance

The most honest use of 503—you're intentionally down:

HTTP/1.1 503 Service Unavailable
Retry-After: Wed, 21 Oct 2024 16:00:00 GMT

{
    "error": "Maintenance in Progress",
    "message": "Scheduled maintenance until 4:00 PM.",
    "resumeTime": "2024-10-21T16:00:00Z"
}

Overload

The server is drowning in requests:

HTTP/1.1 503 Service Unavailable
Retry-After: 30

{
    "error": "Service Overloaded",
    "message": "Too many concurrent requests. Try again in 30 seconds."
}

Dependency Failure

A critical dependency (database, cache, external API) is down:

app.get('/api/data', async function(request, response) {
    try {
        const data = await database.query('SELECT * FROM data');
        response.json(data);
    }
    catch(error) {
        response.status(503)
            .set('Retry-After', '60')
            .json({
                error: 'Service Unavailable',
                message: 'Database temporarily unavailable'
            });
    }
});

Startup and Shutdown

The application isn't ready yet, or is gracefully exiting:

let ready = false;
let shuttingDown = false;

app.use(function(request, response, next) {
    if(!ready || shuttingDown) {
        return response.status(503).json({
            error: ready ? 'Shutting Down' : 'Starting Up',
            message: 'Service temporarily unavailable'
        });
    }
    next();
});

The Retry-After Header

This header transforms 503 from a status into a conversation.

Seconds format for short waits:

Retry-After: 60

Date format for scheduled maintenance:

Retry-After: Wed, 21 Oct 2024 16:00:00 GMT

Without Retry-After, clients are guessing. With it, they know exactly when to return.

Implementing Maintenance Mode

The simplest approach—a file that exists or doesn't:

const fs = require('fs');

app.use(function(request, response, next) {
    if(fs.existsSync('/var/www/maintenance.flag')) {
        const config = JSON.parse(fs.readFileSync('/var/www/maintenance.flag'));
        return response.status(503)
            .set('Retry-After', config.retryAfter)
            .json({
                error: 'Maintenance in Progress',
                message: config.message
            });
    }
    next();
});

Enable maintenance:

echo '{"retryAfter": 3600, "message": "Scheduled maintenance"}' > /var/www/maintenance.flag

Disable:

rm /var/www/maintenance.flag

No deployment required. No restart needed.

Clients Honoring the Promise

A client that respects 503 and Retry-After:

async function fetchWithRetry(url, maxRetries = 3) {
    for(let attempt = 0; attempt < maxRetries; attempt++) {
        const response = await fetch(url);

        if(response.status !== 503) {
            return response;
        }

        const retryAfter = response.headers.get('Retry-After');
        let waitTime;

        if(retryAfter && /^\d+$/.test(retryAfter)) {
            waitTime = parseInt(retryAfter) * 1000;
        }
        else if(retryAfter) {
            waitTime = new Date(retryAfter).getTime() - Date.now();
        }
        else {
            waitTime = Math.pow(2, attempt) * 1000; // Exponential backoff
        }

        await new Promise(function(resolve) {
            setTimeout(resolve, waitTime);
        });
    }

    throw new Error('Service unavailable after retries');
}

Health Checks That Mean Something

Health endpoints should return 503 when unhealthy:

app.get('/health', async function(request, response) {
    const checks = {
        database: await pingDatabase(),
        cache: await pingCache()
    };

    const healthy = Object.values(checks).every(Boolean);

    response.status(healthy ? 200 : 503)
        .set(healthy ? {} : { 'Retry-After': '60' })
        .json({ status: healthy ? 'healthy' : 'unhealthy', checks });
});

Load balancers watch these endpoints. When they see 503, they route traffic elsewhere until the server recovers.

503 vs. Its Neighbors

StatusMeaningRetry?
500Something unexpected brokeMaybe never
502Upstream server misbehavedUnknown
503Temporarily unavailableYes, at Retry-After
429You hit your rate limitYes, server is fine

503 is unique: it's the server admitting temporary weakness while promising recovery. 500 is confusion. 502 is blame. 429 is the client's fault. Only 503 says "this is on me, and I'm handling it."

The Discipline of 503

Always include Retry-After. A 503 without it is a promise without a timeline.

Only use for temporary conditions. If the database schema is wrong, that's a 500. If the database is restarting, that's a 503.

Fail fast. If you know you can't handle a request, return 503 immediately. Don't waste resources discovering what you already knew.

Monitor your 503 rate. Frequent 503s reveal capacity problems, flaky dependencies, or maintenance processes that run too long.

Frequently Asked Questions About 503 Service Unavailable

Was this page helpful?

😔
🤨
😃