HTTP Status Codes: What Every Developer Should Know About Monitoring
Not all 2xx responses are healthy, and not all errors mean downtime. Here's what HTTP status codes actually tell you about your service health.
up0 Team
Status Codes Are Not the Full Story
Your health check returns 200 OK. Your monitoring dashboard is green. But users are seeing blank pages, empty responses, or 30-second load times.
HTTP status codes tell you part of the story. The rest is up to you.
The Five Classes, Briefly
1xx, Informational: The request was received and is being processed. Rarely seen in production monitoring. If you're seeing lots of 100 Continue responses, something upstream is behaving unusually.
2xx, Success: The request was received, understood, and accepted. The most common code you want to see, but "success" doesn't mean "correct."
3xx, Redirection: The client needs to take additional action to complete the request. Redirect chains can silently kill performance.
4xx, Client errors: The request was malformed or unauthorized. These are often legitimate (a 404 for a missing resource, a 401 for an unauthenticated call), but a spike in 4xx errors almost always signals a real problem.
5xx, Server errors: The server failed to fulfill a valid request. These are the ones that wake you up at 3 AM.
Why 200 OK Doesn't Mean Healthy
A 200 response tells you the server processed the request. It does not tell you:
- Whether the response body contains real data
- Whether the response time is acceptable
- Whether the downstream services the endpoint depends on are functioning
Consider an API that returns 200 OK with an empty JSON array because the database query timed out silently. From a status code perspective, everything looks fine. From a user perspective, the page is broken.
What to check beyond the status code:
GET https://api.example.com/v1/users
Expected: 200 OK, body contains at least 1 user
Actual: 200 OK, body is []
→ Silent failure. Status check passes. Users see an empty screen.
Good monitoring validates both the status code and the response body. Check for expected fields, minimum response sizes, or specific strings like "status":"ok".
301 Redirect Chains and Why They Matter
A 301 redirect is a permanent redirect. One redirect is fine. A chain of three or four is a performance problem.
Each redirect adds a full round-trip. In a multi-region setup:
Request → 301 (100ms) → 301 (100ms) → 200 (100ms)
Total: 300ms instead of the expected 100ms
Monitoring tools that follow redirects by default can hide this. Configure your monitors to flag redirect chains longer than one hop, or measure the total time including redirects separately.
5xx Monitoring Strategy
Not all 5xx errors are equal:
- 500 Internal Server Error: Something crashed. Usually a bug or an unhandled exception.
- 502 Bad Gateway: Your reverse proxy can't reach the upstream service. Often a deployment or network issue.
- 503 Service Unavailable: The service is intentionally refusing traffic. Common during deployments or when a service is overloaded.
- 504 Gateway Timeout: The upstream service is alive but too slow. Often a database or downstream API problem.
A single 503 during a rolling deploy is expected. Five 503s in a row is an incident. Your alerting thresholds should reflect this:
Alert: more than 3 consecutive 5xx responses from the same region
Alert: 5xx error rate exceeds 1% over a 5-minute window
4xx Errors Are Worth Watching Too
Teams often ignore 4xx errors because they look like client problems. But a sudden spike in 401 Unauthorized responses could mean your auth token rotation broke. A spike in 404 responses could mean a deployment removed a route that's still being called.
Track your 4xx rate as a baseline and alert on significant deviations:
Normal 404 rate: 0.2% (expected for invalid URLs)
Alert threshold: >2% (something structural changed)
Code Example: Checking Beyond Status Code
Here's what a thorough monitor check looks like in practice:
import httpx
def health_check(url: str) -> dict:
response = httpx.get(url, timeout=5.0, follow_redirects=False)
return {
"status_code": response.status_code,
"response_time_ms": response.elapsed.total_seconds() * 1000,
"redirected": response.status_code in (301, 302, 307, 308),
"body_non_empty": len(response.content) > 0,
"contains_expected": b'"status":"ok"' in response.content,
"healthy": (
response.status_code == 200
and response.elapsed.total_seconds() < 2.0
and b'"status":"ok"' in response.content
)
}
A monitor that only checks status_code == 200 will miss the cases that matter most.
Putting It Together
A practical HTTP monitoring setup should:
- Check the status code, but not stop there
- Validate a key field or pattern in the response body
- Enforce a response time threshold (not just uptime)
- Track redirect count separately from final status
- Alert on 5xx rate trends, not just individual failures
- Baseline 4xx rates and alert on deviations
Status codes are a starting point. Real monitoring goes deeper.
Related reading
- Understanding latency percentiles: P50, P95, P99 explained: status codes tell you what failed; percentiles tell you how slow things got before they did.
- Why multi-region monitoring matters for global applications: a 200 OK from one region can still mean an outage in another.
Want to monitor your services automatically? Join the waitlist →. up0 is in early access.