HTTP 503 vs 500 — Service Unavailable vs Internal Server Error Comparison

http 503 vs 500: 503 is intentional (overload, maintenance) and includes Retry-After, while 500 is an unintentional crash. Why the right choice affects monitoring and SLAs.

5xx

503

Service Unavailable

View full 503 page →

5xx

500

Internal Server Error

View full 500 page →

Quick Cheat Sheet

Aspect 503 Service Unavailable 500 Internal Server Error
Intent Intentional — server knows it's unavailable Unintentional — unexpected crash
Retry-After header Recommended Not applicable
Client should retry? Yes, after delay Maybe, but not necessarily
SLA impact Counts as planned downtime if scheduled Counts as unplanned downtime

The Intent Difference

This pair confuses a lot of devs because both are 5xx and both indicate the server can't fulfill the request. The difference is intent:

503 Service Unavailable (RFC 9110 § 15.6.4) means the server knows it can't serve the request right now and is deliberately saying "no, try later." Maintenance window, overload, capacity exhausted, dependency known to be down — these are all 503 cases.

500 Internal Server Error (RFC 9110 § 15.6.1) means the server tried to handle the request and unexpectedly failed. A bug, an unhandled exception, a corner case nobody planned for.

When to Return 503

  • Scheduled maintenance: serve a maintenance page with 503 and Retry-After: 300
  • Database is in failover: temporarily refuse requests until primary is back
  • Auto-scaler is catching up: rather than spawning more 500s, return 503 with Retry-After
  • Capacity-based shedding: when you're past your safe load threshold, shed gracefully
  • Dependency is down: if your auth provider is unreachable, return 503 (your service is operationally down)

Always include Retry-After (in seconds or as an HTTP-date) when you have any estimate.

When to Return 500

  • An exception bubbled out of a handler unexpectedly
  • A SQL query returned an unparseable result
  • A malformed config caused a crash at request time
  • Anything where you didn't plan to fail

Why This Matters Operationally

  • For PagerDuty/alerting: a 503 spike during a planned maintenance window shouldn't page anyone, but a 500 spike always should. Distinguishing them at the alerting layer requires they actually be issued correctly by the app.
  • For SLAs: many uptime SLAs treat 5xx as downtime, but allow exclusions for scheduled maintenance (which is 503 with sufficient notice).
  • For load balancers: AWS ALB and others may treat 503 as a "temporary back-off" signal and gracefully drain connections, while 500 indicates a flaky target that may need to be replaced.

Common Mistakes

  • Returning 500 for "we're doing maintenance" — this looks like a real outage to monitoring
  • Returning 503 for an unhandled exception that you didn't expect — this hides real bugs
  • Returning 503 without Retry-After — clients will just hammer you immediately

Real-World Examples

  • Gitlab maintenance: returns 503 with a maintenance page during deploys
  • Stripe API: returns 503 with Retry-After when shedding load during traffic spikes
  • Most web frameworks automatically return 500 for any uncaught exception in middleware, which is correct behavior

Real-World Use Case

When deploying a new version of a Rails app, configure a maintenance page that returns 503 with Retry-After: 60 so health checks know to retry and external monitors don't fire incident pages. For unhandled exceptions in production, let the framework return 500 — but make sure they're captured by Sentry/Datadog so you actually fix them, not silently swallow them as 503s.

Look Up Any Status Code

Browse all status codes →