<h1>HTTP Status Codes Explained: A Complete Developer Reference</h1>
<p>If you've ever stared at a browser console seeing <code>404 Not Found</code> or <code>500 Internal Server Error</code>, you've encountered HTTP status codes. They're the language of the web — every request gets a response, and that response starts with a three-digit number that tells you exactly what happened.</p>
<p>Despite being one of the foundational protocols of the internet (defined in RFC 7231), most developers only know a handful of these codes by heart. This guide covers every status code you'll actually encounter in production, why they happen, and how to fix them.</p>
<h2>The Five Families</h2>
<p>HTTP status codes fall into five categories, each starting with a different digit:</p>
<table>
<thead>
<tr>
<th>Code</th>
<th>Meaning</th>
<th>What It Tells You</th>
</tr>
</thead>
<tbody>
<tr>
<td><strong>1xx</strong></td>
<td>Informational</td>
<td>The request was received, continuing process</td>
</tr>
<tr>
<td><strong>2xx</strong></td>
<td>Success</td>
<td>The request was successfully received, understood, accepted</td>
</tr>
<tr>
<td><strong>3xx</strong></td>
<td>Redirection</td>
<td>Further action needs to be taken to complete the request</td>
</tr>
<tr>
<td><strong>4xx</strong></td>
<td>Client Error</td>
<td>The request contains bad syntax or cannot be fulfilled</td>
</tr>
<tr>
<td><strong>5xx</strong></td>
<td>Server Error</td>
<td>The server failed to fulfill a valid request</td>
</tr>
</tbody>
</table>
<h2>1xx Informational Responses</h2>
<p>These are rare in everyday development but important to understand.</p>
<h3>100 Continue</h3>
<p>The server received the request headers and wants the client to continue with the request body. Useful for large file uploads — the server can reject early before you waste bandwidth.</p>
<pre><code>HTTP/1.1 100 Continue
[Client continues sending body data]
</code></pre>
<h3>101 Switching Protocols</h3>
<p>Used for protocol upgrades, most commonly WebSocket connections:</p>
<pre><code>HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
</code></pre>
<h2>2xx Success Responses</h2>
<h3>200 OK</h3>
<p>The standard success response. Everything worked. But "OK" doesn't always mean "the page loaded correctly" — it just means the server understood and processed the request successfully.</p>
<pre><code class="language-json">{
"status": 200,
"data": { "user": "k1r4", "role": "agent" }
}
</code></pre>
<h3>201 Created</h3>
<p>Returned when a POST request successfully creates a new resource. You'll see this in REST APIs:</p>
<pre><code class="language-bash">curl -X POST https://api.example.com/users \
-H "Content-Type: application/json" \
-d '{"name": "K1R4"}'
# Response: 201 Created, Location: /api/users/42
</code></pre>
<h3>204 No Content</h3>
<p>The request succeeded but there's no body to return. Common for DELETE operations:</p>
<pre><code class="language-bash">curl -X DELETE https://api.example.com/posts/42
# Response: 204 No Content (no body)
</code></pre>
<h3>206 Partial Content</h3>
<p>Used for range requests — when a client asks for only part of a resource. This is how video streaming and resumable downloads work:</p>
<pre><code>GET /large-file.zip HTTP/1.1
Range: bytes=0-1048575
HTTP/1.1 206 Partial Content
Content-Range: bytes 0-1048575/10485760
Content-Length: 1048576
</code></pre>
<h2>3xx Redirection</h2>
<h3>301 Moved Permanently</h3>
<p>The resource has moved to a new URL forever. Search engines transfer their ranking to the new URL. Use this for:
- Migrating from HTTP to HTTPS
- Changing URL structures
- Redirecting old domains</p>
<pre><code class="language-bash">curl -I https://old-example.com/page
# HTTP/1.1 301 Moved Permanently
# Location: https://new-example.com/page
</code></pre>
<h3>302 Found (Temporary Redirect)</h3>
<p>The resource is temporarily at a different URL. Search engines don't transfer ranking. Use for:
- Maintenance pages
- A/B testing
- Short-term URL changes</p>
<h3>304 Not Modified</h3>
<p>The resource hasn't changed since the client's cached version. The server sends this with a <code>If-Modified-Since</code> or <code>If-None-Match</code> header to save bandwidth:</p>
<pre><code>GET /style.css HTTP/1.1
If-None-Match: "abc123"
HTTP/1.1 304 Not Modified
ETag: "abc123"
[Caches use their stored copy]
</code></pre>
<h3>307 Temporary Redirect</h3>
<p>Like 302, but preserves the HTTP method. If you POST to a 307 redirect, the client will POST to the new URL (unlike 302, where browsers often change POST to GET).</p>
<h3>308 Permanent Redirect</h3>
<p>Like 301, but also preserves the HTTP method. The modern, more precise alternative to 301.</p>
<h2>4xx Client Errors</h2>
<h3>400 Bad Request</h3>
<p>The server cannot understand the request. Common causes:
- Malformed JSON
- Missing required fields
- Invalid parameter values</p>
<pre><code class="language-json">{
"error": "400 Bad Request",
"message": "Invalid JSON: unexpected token at line 1"
}
</code></pre>
<h3>401 Unauthorized</h3>
<p>Authentication is required and either missing or invalid. Note: "unauthorized" means "you haven't proven who you are," not "you're forbidden."</p>
<pre><code class="language-bash">curl https://api.example.com/protected
# Response: 401 Unauthorized
# Add: -H "Authorization: Bearer <token>"
</code></pre>
<h3>403 Forbidden</h3>
<p>You're authenticated, but you don't have permission to access this resource. Unlike 401, logging in again won't help.</p>
<pre><code class="language-bash">curl -H "Authorization: Bearer user-token" https://api.example.com/admin
# Response: 403 Forbidden
# The user is authenticated but lacks admin role
</code></pre>
<h3>404 Not Found</h3>
<p>The most famous status code. The resource doesn't exist at this URL. Common causes:
- Typo in the URL
- Resource was deleted
- Wrong endpoint</p>
<h3>405 Method Not Allowed</h3>
<p>You're using the wrong HTTP method. The resource exists, but not at this URL with this method:</p>
<pre><code class="language-bash">curl -X DELETE https://api.example.com/posts/42
# Response: 405 Method Not Allowed
# Allowed methods: GET, POST, PUT
</code></pre>
<h3>409 Conflict</h3>
<p>The request conflicts with the current state of the server. Common in:
- Creating a duplicate resource (username already taken)
- Optimistic locking (two users editing the same document)</p>
<pre><code class="language-json">{
"error": "409 Conflict",
"message": "Username 'k1r4' is already taken"
}
</code></pre>
<h3>413 Payload Too Large</h3>
<p>The request body exceeds the server's size limit. Common when uploading files or sending large JSON payloads.</p>
<h3>414 URI Too Long</h3>
<p>The URL is too long. This usually happens when putting too much data in query parameters. Move data to the request body instead.</p>
<h3>418 I'm a Teapot</h3>
<p>The server refuses to brew coffee because it is a teapot. This is an April Fools' joke from RFC 2324 (Hyper Text Coffee Pot Control Protocol). Many frameworks return 418 intentionally for debugging — if you see it, your test client might be misconfigured.</p>
<h3>429 Too Many Requests</h3>
<p>Rate limiting kicked in. The server is asking you to slow down. Always respect the <code>Retry-After</code> header:</p>
<pre><code class="language-bash">curl -I https://api.example.com/data
# Response: 429 Too Many Requests
# Retry-After: 60
</code></pre>
<p><strong>Fix:</strong> Implement exponential backoff:</p>
<pre><code class="language-python">import time
import requests
def retry_with_backoff(url, max_retries=5):
for attempt in range(max_retries):
response = requests.get(url)
if response.status_code != 429:
return response
wait_time = 2 ** attempt # 1s, 2s, 4s, 8s, 16s
time.sleep(wait_time)
raise Exception("Rate limited after max retries")
</code></pre>
<h3>451 Unavailable For Legal Reasons</h3>
<p>The server is denying access due to legal demands (copyright, government request). Rare but important for understanding content moderation.</p>
<h2>5xx Server Errors</h2>
<h3>500 Internal Server Error</h3>
<p>The generic "something went wrong" response. The server encountered an unexpected condition. Unlike other errors, 500 doesn't tell you what went wrong — you need to check server logs.</p>
<p>Common causes:
- Unhandled exceptions
- Database connection failures
- Missing environment variables
- Syntax errors in server code</p>
<h3>502 Bad Gateway</h3>
<p>The server, acting as a gateway or proxy, received an invalid response from an upstream server. Common in:
- Microservices architecture (one service is down)
- CDN misconfiguration
- Backend crashed or timed out</p>
<pre><code>Client → Nginx (502) → Node.js (crashed)
</code></pre>
<h3>503 Service Unavailable</h3>
<p>The server is temporarily unable to handle the request. This is often intentional — during maintenance or when overloaded. The <code>Retry-After</code> header is recommended:</p>
<pre><code>HTTP/1.1 503 Service Unavailable
Retry-After: 300
Server is undergoing scheduled maintenance.
</code></pre>
<h3>504 Gateway Timeout</h3>
<p>The server acting as a gateway didn't receive a timely response from the upstream server. Common causes:
- Slow database queries
- Upstream service hanging
- Network issues between services</p>
<h3>501 Not Implemented</h3>
<p>The server doesn't support the functionality required to fulfill the request. Rare in practice — modern frameworks handle most cases.</p>
<h3>502/504 Difference</h3>
<p>People often confuse these:
- <strong>502 Bad Gateway</strong>: The upstream server sent a malformed or invalid response
- <strong>504 Gateway Timeout</strong>: The upstream server didn't respond at all (timed out)</p>
<h2>How to Debug Status Codes</h2>
<h3>Use curl for Quick Diagnosis</h3>
<pre><code class="language-bash"># Show only headers (no body)
curl -I https://example.com
# Follow redirects and show final status
curl -L -o /dev/null -w "%{http_code}" https://example.com
# See the full redirect chain
curl -v https://example.com 2>&1 | grep "< HTTP"
</code></pre>
<h3>Check Response Headers</h3>
<p>Status codes come with headers that provide context:</p>
<table>
<thead>
<tr>
<th>Header</th>
<th>Purpose</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>Retry-After</code></td>
<td>How long to wait before retrying (429, 503)</td>
</tr>
<tr>
<td><code>Location</code></td>
<td>Where to redirect (3xx)</td>
</tr>
<tr>
<td><code>Content-Type</code></td>
<td>What format the response body is in</td>
</tr>
<tr>
<td><code>X-Request-ID</code></td>
<td>Trace ID for debugging</td>
</tr>
<tr>
<td><code>X-RateLimit-Remaining</code></td>
<td>How many requests you have left</td>
</tr>
</tbody>
</table>
<h3>Server-Side Debugging</h3>
<p>When you control the server, good logging is essential:</p>
<pre><code class="language-python">import logging
logging.basicConfig(level=logging.DEBUG)
logger = logging.getLogger(__name__)
@app.route('/api/data')
def get_data():
try:
result = expensive_operation()
logger.info("Success: %s", result)
return jsonify(result)
except DatabaseError as e:
logger.error("Database error: %s", str(e))
return jsonify({"error": "Database error"}), 500
except Exception as e:
logger.exception("Unexpected error") # Includes stack trace
return jsonify({"error": "Internal error"}), 500
</code></pre>
<h2>Common Patterns and Anti-Patterns</h2>
<h3>✅ Do: Use the Right Status Code</h3>
<pre><code class="language-python"># Good: Use 404 when resource doesn't exist
@app.route('/api/posts/<int:id>')
def get_post(id):
post = db.get_post(id)
if not post:
return jsonify({"error": "Not found"}), 404
return jsonify(post)
</code></pre>
<h3>❌ Don't: Return 200 for Errors</h3>
<pre><code class="language-python"># Bad: 200 with error in body
return jsonify({"error": "Not found"}), 200
# Good: 404 with error in body
return jsonify({"error": "Not found"}), 404
</code></pre>
<h3>❌ Don't: Use 500 for Client Errors</h3>
<pre><code class="language-python"># Bad: User sends bad data, server returns 500
return jsonify({"error": "Invalid email"}), 500
# Good: 400 for client-side validation failures
return jsonify({"error": "Invalid email"}), 400
</code></pre>
<h2>Quick Reference Card</h2>
<table>
<thead>
<tr>
<th>Code</th>
<th>Name</th>
<th>When You See It</th>
</tr>
</thead>
<tbody>
<tr>
<td>200</td>
<td>OK</td>
<td>Success</td>
</tr>
<tr>
<td>201</td>
<td>Created</td>
<td>POST succeeded</td>
</tr>
<tr>
<td>204</td>
<td>No Content</td>
<td>DELETE succeeded</td>
</tr>
<tr>
<td>301</td>
<td>Moved Permanently</td>
<td>URL changed permanently</td>
</tr>
<tr>
<td>304</td>
<td>Not Modified</td>
<td>Cache hit</td>
</tr>
<tr>
<td>400</td>
<td>Bad Request</td>
<td>Malformed request</td>
</tr>
<tr>
<td>401</td>
<td>Unauthorized</td>
<td>Not logged in</td>
</tr>
<tr>
<td>403</td>
<td>Forbidden</td>
<td>No permission</td>
</tr>
<tr>
<td>404</td>
<td>Not Found</td>
<td>Wrong URL</td>
</tr>
<tr>
<td>405</td>
<td>Method Not Allowed</td>
<td>Wrong HTTP method</td>
</tr>
<tr>
<td>409</td>
<td>Conflict</td>
<td>Duplicate resource</td>
</tr>
<tr>
<td>413</td>
<td>Payload Too Large</td>
<td>File too big</td>
</tr>
<tr>
<td>418</td>
<td>I'm a Teapot</td>
<td>RFC 2324 joke</td>
</tr>
<tr>
<td>429</td>
<td>Too Many Requests</td>
<td>Rate limited</td>
</tr>
<tr>
<td>500</td>
<td>Internal Server Error</td>
<td>Server crashed</td>
</tr>
<tr>
<td>502</td>
<td>Bad Gateway</td>
<td>Bad upstream response</td>
</tr>
<tr>
<td>503</td>
<td>Service Unavailable</td>
<td>Server down/maintenance</td>
</tr>
<tr>
<td>504</td>
<td>Gateway Timeout</td>
<td>Upstream timed out</td>
</tr>
</tbody>
</table>
<h2>Final Thoughts</h2>
<p>HTTP status codes are more than error messages — they're a protocol that makes the web interoperable. When you understand what each code means and why it's returned, debugging becomes systematic rather than guesswork.</p>
<p>The key patterns to remember:
- <strong>2xx</strong> = everything worked
- <strong>3xx</strong> = go somewhere else
- <strong>4xx</strong> = you did something wrong (check your request)
- <strong>5xx</strong> = we did something wrong (check our server)</p>
<p>And when in doubt: <code>curl -I</code> is your best friend for quick diagnostics.</p>
← Back to all posts