This error occurs when a server is temporarily unable to handle requests, typically due to being overloaded, down for maintenance, or having unavailable dependencies. In Node.js, you may receive 503 from external APIs or return 503 when your own application cannot accept traffic. This guide covers both scenarios.
The HTTP 503 Service Unavailable error indicates that the server is temporarily unable or unwilling to handle the request. Unlike permanent 5xx errors, 503 is designed to signal transient problems that will resolve. When your Node.js application receives a 503 from an external service, it means that service is overloaded or temporarily offline. When your app returns 503, you're telling clients and load balancers "I'm working but can't take requests right now." In Node.js applications, 503 errors occur in two main contexts: 1. **Receiving 503 from external APIs/microservices**: Your application calls an external service (payment API, database, cache, etc.) that responds with 503. This is temporary and should trigger retry logic. 2. **Your Node.js app returning 503**: Your application is resource-constrained (memory, CPU, connections), has critical dependencies offline, or is intentionally refusing requests during maintenance or shutdown. The key characteristic of 503 is that it includes an optional Retry-After header telling clients when the service should recover.
When receiving a 503 response, check if the server provides a Retry-After header indicating when to retry:
import axios from 'axios';
try {
const response = await axios.get('https://api.example.com/data');
console.log(response.data);
} catch (error) {
if (error.response?.status === 503) {
const retryAfter = error.response.headers['retry-after'];
if (retryAfter) {
if (isNaN(retryAfter)) {
// HTTP date format: Retry-After: Wed, 21 Oct 2025 07:28:00 GMT
const retryDate = new Date(retryAfter);
const waitTime = retryDate.getTime() - Date.now();
console.log(`Service unavailable. Retry after ${waitTime}ms`);
} else {
// Seconds format: Retry-After: 120
const waitSeconds = parseInt(retryAfter);
console.log(`Service unavailable. Retry after ${waitSeconds} seconds`);
}
} else {
console.log('Service unavailable. Retry-After header not provided.');
}
// Check upstream service status page
console.log('Visit https://api.example.com/status for current status');
}
}Before implementing automatic retries, verify the upstream service is actually experiencing issues:
# Check if service is reachable
curl -i https://api.example.com/health
# Check for maintenance announcements
curl https://api.example.com/status
# Monitor DNS resolution
nslookup api.example.comAdd retry logic with exponential backoff to handle temporary unavailability:
import axios from 'axios';
async function fetchWithRetry(url, maxRetries = 3) {
let lastError;
for (let attempt = 1; attempt <= maxRetries; attempt++) {
try {
const response = await axios.get(url, {
timeout: 10000, // 10 second timeout
});
return response.data;
} catch (error) {
lastError = error;
// Only retry on 503 or network errors
if (error.response?.status === 503 || error.code === 'ECONNREFUSED') {
if (attempt < maxRetries) {
// Exponential backoff: 1s, 2s, 4s
const delayMs = Math.pow(2, attempt - 1) * 1000;
const retryAfter = error.response?.headers['retry-after'];
// Use Retry-After header if provided
const waitMs = retryAfter ?
(isNaN(retryAfter) ?
new Date(retryAfter).getTime() - Date.now() :
parseInt(retryAfter) * 1000) :
delayMs;
console.log(`Attempt ${attempt} failed. Retrying in ${waitMs}ms...`);
await new Promise(resolve => setTimeout(resolve, waitMs));
continue;
}
} else {
// Non-retryable error, throw immediately
throw error;
}
}
}
throw lastError;
}
// Usage
try {
const data = await fetchWithRetry('https://api.example.com/data', 4);
console.log('Data retrieved:', data);
} catch (error) {
console.error('Failed after all retries:', error.message);
}For Axios, you can also use the axios-retry library:
import axios from 'axios';
import axiosRetry from 'axios-retry';
axiosRetry(axios, {
retries: 3,
retryDelay: axiosRetry.exponentialDelay,
retryCondition: (error) => {
// Retry on 503 or network errors
return axiosRetry.isNetworkOrIdempotentRequestError(error) ||
error.response?.status === 503;
},
});
const response = await axios.get('https://api.example.com/data');When your Node.js application calls external services, handle 503 gracefully and propagate it appropriately:
import express from 'express';
import axios from 'axios';
const app = express();
app.get('/api/user/:id', async (req, res, next) => {
try {
const userData = await axios.get(`https://user-service.example.com/api/users/${req.params.id}`, {
timeout: 8000,
});
res.json(userData.data);
} catch (error) {
if (error.response?.status === 503) {
// Service temporarily unavailable - return 503 with Retry-After
const retryAfter = error.response.headers['retry-after'] || '60';
res.set('Retry-After', retryAfter);
return res.status(503).json({
error: 'Service Unavailable',
message: 'User service is temporarily unavailable. Please try again later.',
retryAfter: parseInt(retryAfter),
});
}
// Other errors
next(error);
}
});
// Error handler for service unavailability
app.use((err, req, res, next) => {
if (err.message?.includes('503') || err.status === 503) {
return res.status(503).json({
error: 'Service Unavailable',
message: 'The service is temporarily unavailable',
retryable: true,
});
}
next(err);
});Use a circuit breaker to stop overwhelming an unavailable service and prevent cascading failures:
import CircuitBreaker from 'opossum';
import axios from 'axios';
// Define the external API call
async function callUserService(userId) {
const response = await axios.get(`https://user-service.example.com/api/users/${userId}`, {
timeout: 5000,
});
return response.data;
}
// Wrap with circuit breaker
const breaker = new CircuitBreaker(callUserService, {
timeout: 5000, // Individual request timeout
errorThresholdPercentage: 50, // Open circuit after 50% failures
resetTimeout: 30000, // Try again after 30 seconds
name: 'user-service',
});
// Handle circuit state changes
breaker.on('open', () => {
console.warn('Circuit breaker OPEN - User service unavailable, stopping requests');
});
breaker.on('halfOpen', () => {
console.log('Circuit breaker HALF_OPEN - Testing if service recovered');
});
breaker.on('close', () => {
console.log('Circuit breaker CLOSED - Service recovered');
});
// Express middleware
app.get('/api/user/:id', async (req, res) => {
try {
const user = await breaker.fire(req.params.id);
res.json(user);
} catch (error) {
if (error.message.includes('OPEN')) {
// Serve stale cached data or fallback response
res.status(503).json({
error: 'Service Unavailable',
message: 'User service is currently unavailable',
});
} else {
res.status(500).json({ error: error.message });
}
}
});Install the circuit breaker library:
npm install opossumIf your Node.js app is returning 503 responses, investigate resource constraints:
import os from 'os';
import { performance } from 'perf_hooks';
// Monitor memory usage
function checkMemory() {
const used = process.memoryUsage();
const totalMemory = os.totalmem();
const freeMemory = os.freemem();
console.log(`Memory: ${Math.round(used.heapUsed / 1024 / 1024)}MB / ${Math.round(used.heapTotal / 1024 / 1024)}MB`);
console.log(`System: ${Math.round(freeMemory / 1024 / 1024)}MB free / ${Math.round(totalMemory / 1024 / 1024)}MB total`);
// Return true if memory usage too high
if (used.heapUsed / used.heapTotal > 0.9) {
return true; // Should return 503
}
}
// Monitor CPU usage
function checkCPU() {
const cpus = os.cpus();
let totalIdle = 0, totalTick = 0;
cpus.forEach((cpu) => {
for (type in cpu.times) {
totalTick += cpu.times[type];
}
totalIdle += cpu.times.idle;
});
const idle = totalIdle / cpus.length;
const total = totalTick / cpus.length;
const usage = 100 - ~~(100 * idle / total);
return usage > 95; // Return 503 if CPU > 95%
}
// Middleware to check resources
app.use((req, res, next) => {
if (checkMemory() || checkCPU()) {
return res.status(503).json({
error: 'Service Unavailable',
message: 'Server is currently overloaded',
});
}
next();
});Check database connection pool:
import { PrismaClient } from '@prisma/client';
const prisma = new PrismaClient({
log: [
{ emit: 'event', level: 'warn' },
{ emit: 'event', level: 'error' },
],
});
prisma.$on('warn', (e) => {
console.warn('Database warning:', e.message);
// If connection pool exhausted, connections start queuing
});
// Monitor active connections
app.get('/health', async (req, res) => {
try {
// This will fail if database is unavailable
const result = await prisma.$queryRaw`SELECT 1`;
res.json({ status: 'healthy', database: 'connected' });
} catch (error) {
res.status(503).json({
status: 'unavailable',
database: 'disconnected',
error: error.message,
});
}
});Implement health checks to signal when your app is overloaded and can't accept traffic:
import express from 'express';
import http from 'http';
const app = express();
let isShuttingDown = false;
let activeConnections = 0;
const server = http.createServer(app);
// Track active connections
server.on('connection', (conn) => {
activeConnections++;
conn.on('close', () => {
activeConnections--;
});
});
// Health check endpoint
app.get('/health', (req, res) => {
const used = process.memoryUsage();
const heapUsagePercent = used.heapUsed / used.heapTotal;
if (isShuttingDown) {
return res.status(503).json({
status: 'shutting-down',
message: 'Server is shutting down, no new requests accepted',
});
}
if (heapUsagePercent > 0.85 || activeConnections > 1000) {
return res.status(503).json({
status: 'overloaded',
memory: `${Math.round(heapUsagePercent * 100)}%`,
activeConnections,
});
}
res.json({
status: 'healthy',
uptime: process.uptime(),
memory: `${Math.round(heapUsagePercent * 100)}%`,
activeConnections,
});
});
// Graceful shutdown
process.on('SIGTERM', () => {
console.log('SIGTERM received, shutting down gracefully...');
isShuttingDown = true;
server.close(() => {
console.log('Server closed');
process.exit(0);
});
// Force shutdown after 30 seconds
setTimeout(() => {
console.error('Forced shutdown');
process.exit(1);
}, 30000);
});
server.listen(3000);Difference Between 503 and Other 5xx Errors
503 Service Unavailable is specifically designed for temporary conditions:
- 502 Bad Gateway: Upstream server is unreachable or responds with invalid data (not necessarily retryable)
- 503 Service Unavailable: Server is temporarily unable to handle requests (clients should retry)
- 504 Gateway Timeout: Request took too long (usually requires timeout adjustments, not retries)
Only 503 responses should include a Retry-After header and should use that header to guide retry logic.
Implementing Comprehensive Error Handling
Handle both receiving and sending 503 errors properly:
import axios from 'axios';
import express from 'express';
const app = express();
// When calling external APIs
async function callExternalAPI(endpoint) {
try {
const response = await axios.get(endpoint, {
timeout: 10000,
validateStatus: (status) => status < 500 || status === 503, // Handle all errors
});
if (response.status === 503) {
throw new ServiceUnavailableError(
'External service unavailable',
response.headers['retry-after']
);
}
return response.data;
} catch (error) {
if (error instanceof ServiceUnavailableError) {
throw error;
}
throw new Error(`API call failed: ${error.message}`);
}
}
class ServiceUnavailableError extends Error {
constructor(message, retryAfter) {
super(message);
this.name = 'ServiceUnavailableError';
this.retryAfter = retryAfter;
this.retryable = true;
}
}
// When handling requests in your app
app.get('/api/endpoint', async (req, res, next) => {
try {
const data = await callExternalAPI('https://api.example.com/data');
res.json(data);
} catch (error) {
if (error instanceof ServiceUnavailableError) {
res.set('Retry-After', error.retryAfter || '60');
return res.status(503).json({
error: 'Service Unavailable',
message: error.message,
retryable: true,
retryAfterSeconds: error.retryAfter,
});
}
next(error);
}
});
// Global 503 handler
app.use((err, req, res, next) => {
if (err instanceof ServiceUnavailableError) {
res.set('Retry-After', err.retryAfter || '60');
return res.status(503).json({
error: 'Service Unavailable',
message: err.message,
retryable: true,
});
}
next(err);
});Production Monitoring for 503 Errors
Use observability tools to track 503 errors and identify patterns:
import * as Sentry from '@sentry/node';
Sentry.init({
dsn: process.env.SENTRY_DSN,
tracesSampleRate: 0.1,
});
app.use(async (req, res, next) => {
try {
next();
} catch (error) {
if (error.response?.status === 503) {
// Track 503 errors separately
Sentry.captureException(error, {
level: 'warning',
tags: {
error_type: '503_upstream_unavailable',
service: error.config?.url,
},
});
} else {
Sentry.captureException(error);
}
throw error;
}
});
// Monitor response status codes
app.use((req, res, next) => {
const originalSend = res.send;
res.send = function (data) {
if (res.statusCode === 503) {
console.warn(`503 response sent to ${req.path}`);
}
return originalSend.call(this, data);
};
next();
});Error: EMFILE: too many open files, watch
EMFILE: fs.watch() limit exceeded
Error: Middleware next() called multiple times (next() invoked twice)
Express middleware next() called multiple times
Error: Worker failed to initialize (worker startup error)
Worker failed to initialize in Node.js
Error: EMFILE: too many open files, open 'file.txt'
EMFILE: too many open files
Error: cluster.fork() failed (cannot create child process)
cluster.fork() failed - Cannot create child process