HTTP 200 OK: What It Means, When It Lies, and How to Monitor It
HTTP 200 OK means the request succeeded — but a 200 doesn't always mean your service is healthy. Learn what 200 OK actually returns, the difference between 200 and 201/204, and how to configure monitoring that catches problems behind a 200.
HTTP 200 OK is the most common status code on the web. It means the server received the request, understood it, and returned the requested content. For most requests — loading a page, calling an API, fetching an image — 200 is the success state.
But 200 is also the most misleading status code in production monitoring. A load balancer can return 200 while your application server is completely down. A CDN can return 200 with a cached error page from three hours ago. A health endpoint can return 200 with a JSON body that says "database": "disconnected".
Understanding what 200 actually means — and what it doesn't — is fundamental to building reliable monitoring.
What 200 OK Means
The HTTP specification (RFC 9110) defines 200 OK as:
The request has succeeded. The content sent in a 200 response depends on the request method.
For different request methods:
| Method | What a 200 response contains |
|---|---|
| GET | The requested resource |
| POST | The result of the action (created resource, confirmation, etc.) |
| PUT | The updated resource, or a representation of the update |
| PATCH | The updated resource, or confirmation of the partial update |
| HEAD | Headers only, no body (identical to GET headers) |
| OPTIONS | Supported methods and headers, no body |
| DELETE | Confirmation of deletion (though 204 is more appropriate) |
200 vs. 201 vs. 204: Choosing the Right 2xx Code
Many APIs return 200 for every successful operation. That works, but using the more specific 2xx codes communicates intent more clearly.
| Code | Name | Use it when |
|---|---|---|
| 200 | OK | Returning requested data (GET) or confirming an action with a body |
| 201 | Created | A new resource was created (POST that creates) |
| 202 | Accepted | Request received and queued, but not yet processed |
| 204 | No Content | Request succeeded but there's nothing to return (DELETE, some PATCHes) |
| 206 | Partial Content | Returning part of a resource (range requests, pagination, streaming) |
Real examples
# GET a user — return 200 with the user object
GET /users/123
→ 200 OK
{"id": 123, "name": "Alice"}
# POST to create a user — return 201, not 200
POST /users
→ 201 Created
Location: /users/124
{"id": 124, "name": "Bob"}
# DELETE a user — return 204, no body needed
DELETE /users/123
→ 204 No Content
# Async job submission — return 202
POST /reports/generate
→ 202 Accepted
{"job_id": "abc123", "status_url": "/jobs/abc123"}
Returning 201 instead of 200 after resource creation costs nothing and makes your API more predictable for consumers. Returning 204 after deletion sets the right expectation that there's no body to parse.
The 200 OK Trap in Production Monitoring
Here's the dangerous scenario: your uptime monitor checks your production URL every minute, gets a 200, and reports "all systems operational." But your users are seeing errors.
How does this happen? Several ways.
CDN caching stale content
If your CDN caches a 200 response from your application, it keeps serving that cached response even after your application goes down. Your monitor hits the CDN edge node, gets a 200 with cached content from four hours ago, and reports healthy. Your users who aren't hitting cache — or users in regions where cache missed — get errors.
Fix: Monitor an uncacheable endpoint. Add Cache-Control: no-store to your health check endpoint and verify your monitor hits it with a cache-busting query parameter if needed.
Load balancer returning its own error page
When your application server crashes, your load balancer (nginx, HAProxy, AWS ALB) often returns a 200 with its own default error page — not a 503 or 502. This happens when the load balancer serves a static "application error" HTML file rather than proxying to the crashed upstream.
Fix: Use body validation in your monitor. Instead of accepting any 200, require the response body to contain a specific string (e.g., "status":"ok") that only your application generates.
Partial application health
Your application returns 200 for the health endpoint, but the database connection is failing, a background job queue is stalled, or a third-party API dependency is down. The endpoint is "up" but the service isn't fully functional.
Fix: Build a real health endpoint — not just return 200. Check critical dependencies and return a meaningful response body.
Building a Health Endpoint That 200 Actually Means Healthy
A minimal health endpoint that's worth monitoring:
// Express / Node.js
app.get('/health', async (req, res) => {
const checks = {
database: 'ok',
cache: 'ok',
queue: 'ok'
}
try {
await db.query('SELECT 1')
} catch {
checks.database = 'error'
}
try {
await redis.ping()
} catch {
checks.cache = 'error'
}
const healthy = Object.values(checks).every(v => v === 'ok')
res.status(healthy ? 200 : 503).json({
status: healthy ? 'ok' : 'degraded',
checks
})
})
# Flask / Python
@app.route('/health')
def health():
checks = {}
try:
db.session.execute('SELECT 1')
checks['database'] = 'ok'
except Exception:
checks['database'] = 'error'
healthy = all(v == 'ok' for v in checks.values())
return jsonify({
'status': 'ok' if healthy else 'degraded',
'checks': checks
}), 200 if healthy else 503
With this pattern, your health endpoint returns:
200 {"status":"ok"}when everything is working503 {"status":"degraded", "checks":{"database":"error"}}when something is wrong
Your monitor checks the status code (200 vs. 503) and the body content. A 200 response that doesn't contain "status":"ok" triggers an alert.
How 200 OK Appears in HTTP Headers
A minimal 200 response:
HTTP/1.1 200 OK
Content-Type: application/json
Content-Length: 16
Date: Thu, 26 Jun 2026 10:00:00 GMT
{"status": "ok"}
Key headers that accompany 200 responses:
| Header | Purpose |
|---|---|
Content-Type | MIME type of the response body (application/json, text/html, etc.) |
Content-Length | Byte length of the body |
Cache-Control | How long proxies and browsers can cache this response |
ETag | Identifier for this version of the resource (for conditional requests) |
Last-Modified | When the resource was last changed |
200 vs. 304: The Caching Case
When a browser or proxy has a cached version of a resource, it sends a conditional request:
GET /api/data HTTP/1.1
If-None-Match: "abc123"
If the resource hasn't changed, the server returns 304 Not Modified with no body — the client uses its cached version. If it has changed, the server returns 200 OK with the new content.
From a monitoring perspective: if your uptime monitor sends conditional requests and receives 304, treat it as healthy. The resource exists and hasn't changed.
200 in API Design: Common Patterns
Returning 200 for errors — Some APIs return 200 with an error in the body:
// Bad: HTTP 200 with an application-level error
HTTP/1.1 200 OK
{"error": "User not found", "code": 404}
This breaks HTTP semantics. HTTP clients, monitoring tools, and API gateways rely on status codes to classify responses. Use 404 Not Found for a missing resource, not 200 OK {"error": "not found"}.
Envelope vs. flat responses — Some APIs wrap all responses in an envelope:
// Envelope pattern
{"success": true, "data": {"id": 123, "name": "Alice"}}
{"success": false, "error": "Validation failed"}
If you use an envelope, your monitoring body validation should check "success": true, not just the status code. A 200 with "success": false in an envelope API is a failure state.
Monitoring Checklist for 200 OK
Before you trust that your service is healthy because it returns 200, verify:
- The endpoint bypasses CDN caching (or cache returns are intentionally tested separately)
- The response body contains a known healthy string (e.g.,
"status":"ok") - The health endpoint checks critical dependencies (database, cache, queues)
- Your monitor follows redirects and checks the final destination's status
- The endpoint is not behind authentication that blocks your monitoring probe IPs
- Response time is within expected range (a 200 after 30 seconds is a problem)
A 200 status code is the beginning of a health check, not the end of it.