What Every HTTP Status Code Means (With Examples)
What Every HTTP Status Code Means (With Examples)
Every time your browser requests a web page, calls an API, or submits a form, the server responds with an HTTP status code. These three-digit numbers are the universal language of the web — they tell you whether your request succeeded, failed, or needs to go somewhere else entirely. Understanding them is not optional for developers. Misinterpreting a 401 versus a 403, or confusing a 302 with a 301, can lead to bugs that are maddeningly difficult to track down.
This guide covers the 18 most important HTTP status codes you'll encounter in real-world development. For each one, you'll get a plain-English explanation, a realistic scenario where it appears, and practical code or configuration examples showing how to handle it. Whether you're debugging a failing API call, configuring redirects, or building error handling into your application, this is the reference you'll keep coming back to.
2xx — Success
Status codes in the 200 range mean the request was received, understood, and processed successfully. These are the codes you want to see. But even within the success family, the specific code matters — a 200 and a 201 communicate very different things to the client.
200 OK
The most common status code on the web. A 200 means the server successfully processed the request and is returning the requested data. For a GET request, the response body contains the resource. For a POST request, it contains the result of the action.
Real-world scenario:You call a REST API to fetch a user's profile, and the server returns the profile data.
// Fetching data from an API
const response = await fetch('/api/users/42');
if (response.status === 200) {
const user = await response.json();
console.log(user.name); // "Jane Doe"
}A 200 response should always include a body. If you're building an API and your successful operation has no data to return, use 204 instead.
201 Created
A 201 tells the client that the request succeeded and a new resource was created as a result. This is the correct response for successful POST requests that create something — a new user account, a new blog post, a new database record. The response should include a Location header pointing to the newly created resource.
Real-world scenario: A client submits a form to create a new task in a project management app.
// Express.js route handler
app.post('/api/tasks', async (req, res) => {
const task = await Task.create({
title: req.body.title,
assignee: req.body.assignee,
});
res.status(201)
.location(`/api/tasks/${task.id}`)
.json(task);
});Common mistake: Returning 200 for resource creation. While it technically works, 201 gives the client more semantic information and is considered best practice in RESTful API design.
204 No Content
The server successfully processed the request, but there's no content to send back. This is the ideal response for DELETE operations and PUT/PATCH updates where the client doesn't need the updated resource echoed back.
Real-world scenario:A user deletes a notification. The server removes it and confirms success, but there's nothing meaningful to return.
// Handling a DELETE request
app.delete('/api/notifications/:id', async (req, res) => {
await Notification.destroy({ where: { id: req.params.id } });
res.status(204).send(); // No body
});
// Client-side handling
const response = await fetch('/api/notifications/15', {
method: 'DELETE',
});
if (response.status === 204) {
// Success — remove the notification from the UI
removeNotificationFromList(15);
}Important: a 204 response must not include a response body. If you accidentally send one, some HTTP clients will throw parsing errors.
3xx — Redirection
Redirection codes tell the client that the resource has moved and the request needs to go somewhere else. Getting these right is critical for SEO, user experience, and avoiding infinite redirect loops.
301 Moved Permanently
The resource has permanently moved to a new URL. Search engines will transfer the old URL's ranking to the new one. Browsers and proxies will cache this redirect aggressively — once a client sees a 301, it may never request the old URL again.
Real-world scenario: You restructure your website's URL scheme, moving blog posts from /blog/123 to /posts/my-post-title.
# Nginx configuration
server {
# Redirect old blog URLs to new structure
location /blog/123 {
return 301 /posts/understanding-http-status-codes;
}
# Redirect entire old path pattern
location ~ ^/blog/(.*)$ {
return 301 /posts/$1;
}
}When to use: Domain migrations, permanent URL restructuring, enforcing HTTPS (redirect HTTP to HTTPS with 301). When NOT to use: Temporary maintenance redirects, A/B testing — use 302 for those.
302 Found (Temporary Redirect)
The resource is temporarily available at a different URL. Unlike 301, the client should continue using the original URL for future requests. Search engines will not transfer ranking authority to the new URL.
Real-world scenario: A user tries to access a dashboard without being logged in. You redirect them to the login page temporarily.
// Next.js middleware for auth redirect
import { NextResponse } from 'next/server';
export function middleware(request) {
const token = request.cookies.get('session');
if (!token && request.nextUrl.pathname.startsWith('/dashboard')) {
const loginUrl = new URL('/login', request.url);
loginUrl.searchParams.set('redirect', request.nextUrl.pathname);
return NextResponse.redirect(loginUrl, 302);
}
}Pro tip: Always include a redirect query parameter so you can send the user back to their intended destination after login. This small detail drastically improves user experience.
304 Not Modified
The resource hasn't changed since the last time the client requested it. The server sends no body — the client should use its cached copy. This is a key part of HTTP caching and dramatically reduces bandwidth usage.
Real-world scenario: A browser requests a CSS file it already has cached. The server checks the If-Modified-Since or ETag header and confirms nothing has changed.
# Client sends conditional request
GET /styles/main.css HTTP/1.1
If-None-Match: "abc123"
# Server responds — no body needed
HTTP/1.1 304 Not Modified
ETag: "abc123"
Cache-Control: max-age=3600You rarely need to handle 304 manually — browsers and HTTP clients manage this automatically. But understanding it helps you debug caching issues. If you're seeing stale content, a 304 response when you expect a 200 might be the culprit. Clear the cache or send a request without conditional headers to force a fresh response.
4xx — Client Errors
These codes indicate that the client made an error — bad syntax, invalid credentials, requesting something that doesn't exist. The 4xx range is where most debugging time is spent, and getting the right code helps API consumers fix their requests faster.
400 Bad Request
The server cannot process the request due to malformed syntax, invalid parameters, or missing required fields. This is the catch-all for "your request doesn't make sense."
Real-world scenario: An API client sends a POST request with a JSON body where the email field contains an invalid format.
// Server-side validation
app.post('/api/users', (req, res) => {
const { email, name } = req.body;
if (!email || !email.includes('@')) {
return res.status(400).json({
error: 'Bad Request',
message: 'Invalid email format. Email must contain an @ symbol.',
field: 'email',
});
}
if (!name || name.trim().length === 0) {
return res.status(400).json({
error: 'Bad Request',
message: 'Name is required and cannot be empty.',
field: 'name',
});
}
// proceed with user creation...
});How to fix:Check the response body for details about what's wrong. Validate your request payload against the API documentation. Ensure JSON is properly formatted and all required fields are included.
401 Unauthorized
Despite the name, 401 actually means unauthenticated. The server doesn't know who you are. You either didn't send credentials, or the credentials you sent are invalid (expired token, wrong password, missing API key).
Real-world scenario: An API call fails because the access token has expired.
// Client-side token refresh pattern
async function fetchWithAuth(url, options = {}) {
let response = await fetch(url, {
...options,
headers: {
...options.headers,
'Authorization': `Bearer ${getAccessToken()}`,
},
});
if (response.status === 401) {
// Token expired — try refreshing
const newToken = await refreshAccessToken();
if (newToken) {
response = await fetch(url, {
...options,
headers: {
...options.headers,
'Authorization': `Bearer ${newToken}`,
},
});
} else {
// Refresh failed — redirect to login
window.location.href = '/login';
}
}
return response;
}How to fix:Verify your API key or token is correct and hasn't expired. Check that you're sending it in the right header format. For Bearer tokens, ensure there's a space between "Bearer" and the token value.
403 Forbidden
The server knows who you are (you're authenticated), but you don't have permission to access this resource. Unlike 401, re-authenticating won't help — you simply lack the required role or privilege.
Real-world scenario: A regular user tries to access an admin-only endpoint.
// Middleware for role-based access control
function requireRole(role) {
return (req, res, next) => {
if (!req.user) {
return res.status(401).json({ error: 'Authentication required' });
}
if (req.user.role !== role) {
return res.status(403).json({
error: 'Forbidden',
message: `This action requires the "${role}" role. Your role: "${req.user.role}".`,
});
}
next();
};
}
app.delete('/api/users/:id', requireRole('admin'), async (req, res) => {
// Only admins reach this code
});How to fix:Contact the system administrator to request the necessary permissions. If you're building the API, make sure your error message explains which permission is missing — "Forbidden" alone is not helpful.
404 Not Found
The server cannot find the requested resource. This is the most recognizable HTTP status code — everyone has seen a 404 page. It can mean the URL is wrong, the resource was deleted, or the route simply doesn't exist.
Real-world scenario: A user bookmarks a product page, but the product is later removed from the catalog.
// API route with proper 404 handling
app.get('/api/products/:id', async (req, res) => {
const product = await Product.findById(req.params.id);
if (!product) {
return res.status(404).json({
error: 'Not Found',
message: `No product found with ID "${req.params.id}".`,
});
}
res.json(product);
});How to fix:Double-check the URL for typos. Verify the resource ID exists. If you recently migrated URLs, set up 301 redirects from old paths to new ones. For APIs, check that you're using the correct base URL and API version.
405 Method Not Allowed
The URL exists, but the HTTP method you used (GET, POST, PUT, DELETE, etc.) is not supported for that endpoint. The server should return an Allow header listing the methods that are supported.
Real-world scenario: A client sends a DELETE request to a read-only resource endpoint that only supports GET.
// Next.js API route with method checking
export default function handler(req, res) {
if (req.method === 'GET') {
const stats = getPublicStats();
return res.status(200).json(stats);
}
// Any other method is not allowed
res.setHeader('Allow', 'GET');
return res.status(405).json({
error: 'Method Not Allowed',
message: `${req.method} is not supported. Use GET instead.`,
});
}How to fix: Check the Allow header in the response to see which methods are supported. Review the API documentation for the correct method. A common mistake is using GET when the endpoint requires POST, or vice versa.
409 Conflict
The request conflicts with the current state of the server. This typically happens when you try to create a resource that already exists, or update a resource that was modified by someone else since you last read it (an optimistic concurrency conflict).
Real-world scenario: Two users edit the same document simultaneously, and the second save conflicts with the first.
// Optimistic concurrency control
app.put('/api/documents/:id', async (req, res) => {
const { content, lastModifiedVersion } = req.body;
const doc = await Document.findById(req.params.id);
if (doc.version !== lastModifiedVersion) {
return res.status(409).json({
error: 'Conflict',
message: 'This document was modified by another user. Please refresh and try again.',
currentVersion: doc.version,
yourVersion: lastModifiedVersion,
});
}
doc.content = content;
doc.version += 1;
await doc.save();
res.json(doc);
});How to fix: Fetch the latest version of the resource, merge your changes, and retry the request. For unique constraint violations (e.g., duplicate email), use a different value or check for existence before creating.
422 Unprocessable Entity
The server understands the request format (it's valid JSON, proper headers), but the content is semantically invalid. Think of 400 as "I can't parse what you sent" and 422 as "I can parse it, but the data doesn't make business sense."
Real-world scenario: A user tries to book a flight with a departure date in the past.
// Business logic validation
app.post('/api/bookings', (req, res) => {
const { departureDate, returnDate, passengers } = req.body;
const errors = [];
if (new Date(departureDate) < new Date()) {
errors.push({ field: 'departureDate', message: 'Departure date cannot be in the past.' });
}
if (returnDate && new Date(returnDate) <= new Date(departureDate)) {
errors.push({ field: 'returnDate', message: 'Return date must be after departure date.' });
}
if (passengers < 1 || passengers > 9) {
errors.push({ field: 'passengers', message: 'Passenger count must be between 1 and 9.' });
}
if (errors.length > 0) {
return res.status(422).json({
error: 'Unprocessable Entity',
message: 'Validation failed. See details below.',
details: errors,
});
}
// proceed with booking...
});How to fix: Read the validation errors in the response body. Fix each field to meet the business rules. This is different from a 400 — your request was syntactically correct but logically invalid.
429 Too Many Requests
You've hit a rate limit. The server is telling you to slow down. The response should include a Retry-After header indicating how many seconds to wait before trying again.
Real-world scenario: An automated script hammers an API endpoint faster than the rate limit allows.
// Client-side retry with exponential backoff
async function fetchWithRetry(url, options = {}, maxRetries = 3) {
for (let attempt = 0; attempt <= maxRetries; attempt++) {
const response = await fetch(url, options);
if (response.status === 429) {
const retryAfter = response.headers.get('Retry-After');
const waitSeconds = retryAfter ? parseInt(retryAfter, 10) : Math.pow(2, attempt);
console.warn(`Rate limited. Retrying in ${waitSeconds} seconds...`);
await new Promise(resolve => setTimeout(resolve, waitSeconds * 1000));
continue;
}
return response;
}
throw new Error('Max retries exceeded due to rate limiting.');
}How to fix: Implement exponential backoff in your client code. Respect the Retry-After header. If you consistently hit rate limits, consider batching requests, caching responses, or requesting a higher rate limit from the API provider.
5xx — Server Errors
Server errors mean something went wrong on the server's side — not your fault as a client. But you still need to handle them gracefully. These errors are often transient, so retry logic is usually appropriate.
500 Internal Server Error
The generic "something broke" error. The server encountered an unexpected condition and couldn't fulfill the request. This could be an unhandled exception, a database connection failure, a null pointer, or any number of bugs.
Real-world scenario: A server tries to access a property on a null object because a database query returned no results and nobody checked for that case.
// BAD: This will cause a 500 error
app.get('/api/users/:id/profile', async (req, res) => {
const user = await User.findById(req.params.id);
res.json({ name: user.name }); // Crashes if user is null!
});
// GOOD: Proper error handling
app.get('/api/users/:id/profile', async (req, res) => {
try {
const user = await User.findById(req.params.id);
if (!user) {
return res.status(404).json({ error: 'User not found' });
}
res.json({ name: user.name });
} catch (err) {
console.error('Database error:', err);
res.status(500).json({
error: 'Internal Server Error',
message: 'An unexpected error occurred. Please try again later.',
});
}
});How to fix (as a client): Retry the request after a brief delay. If the error persists, contact the API provider. How to fix (as a server developer): Check your server logs immediately. Add proper error handling, null checks, and try-catch blocks around database calls and external service integrations.
502 Bad Gateway
A 502 means a server acting as a gateway or proxy received an invalid response from the upstream server it was trying to reach. You see this when a reverse proxy (like Nginx or a load balancer) can't get a valid response from your application server.
Real-world scenario: Your Node.js application crashes during deployment, and Nginx has no healthy backend to forward requests to.
# Nginx configuration to handle upstream failures gracefully
upstream app_servers {
server 127.0.0.1:3000;
server 127.0.0.1:3001 backup;
}
server {
location / {
proxy_pass http://app_servers;
proxy_connect_timeout 5s;
proxy_read_timeout 30s;
# Custom error page for 502
error_page 502 /502.html;
}
}How to fix: Check that your application server is running and listening on the expected port. Review application logs for crashes. Verify your proxy configuration is pointing to the correct upstream address and port. During deployments, use rolling restarts to avoid downtime.
503 Service Unavailable
The server is temporarily unable to handle the request, usually due to maintenance or overload. Unlike a 500, this implies the condition is temporary and the service will be back. The server should include a Retry-After header.
Real-world scenario: A popular product launch causes a traffic spike that overwhelms the server, or a planned maintenance window takes the service offline.
// Express middleware for maintenance mode
const MAINTENANCE_MODE = process.env.MAINTENANCE_MODE === 'true';
app.use((req, res, next) => {
if (MAINTENANCE_MODE) {
res.setHeader('Retry-After', '3600'); // Try again in 1 hour
return res.status(503).json({
error: 'Service Unavailable',
message: 'We are currently performing scheduled maintenance. Please try again later.',
estimatedReturn: '2026-04-22T14:00:00Z',
});
}
next();
});How to fix (as a client): Wait and retry. Honor the Retry-After header if present. Implement a circuit breaker pattern for services that frequently return 503. How to fix (as a server): Scale your infrastructure, add load balancing, implement request queuing, or use auto-scaling to handle traffic spikes.
504 Gateway Timeout
Similar to 502, but instead of an invalid response, the gateway received no response at all within the timeout period. The upstream server is taking too long. This is common when a backend performs a heavy computation or makes a slow third-party API call.
Real-world scenario:An API endpoint generates a complex report by querying multiple databases, and the operation takes longer than the proxy's timeout allows.
// Solution 1: Increase proxy timeout for specific routes
// Nginx config
// location /api/reports {
// proxy_pass http://app_server;
// proxy_read_timeout 120s; # Allow 2 minutes for reports
// }
// Solution 2: Use async processing pattern
app.post('/api/reports', async (req, res) => {
const jobId = await ReportQueue.add({
type: req.body.reportType,
filters: req.body.filters,
});
// Return immediately with a job ID
res.status(202).json({
message: 'Report generation started.',
jobId: jobId,
statusUrl: `/api/reports/status/${jobId}`,
});
});
// Client polls for completion
app.get('/api/reports/status/:jobId', async (req, res) => {
const job = await ReportQueue.getJob(req.params.jobId);
if (job.status === 'completed') {
res.json({ status: 'completed', downloadUrl: job.resultUrl });
} else if (job.status === 'failed') {
res.json({ status: 'failed', error: job.error });
} else {
res.json({ status: 'processing', progress: job.progress });
}
});How to fix: For long-running operations, switch to an asynchronous processing pattern — accept the request, return a 202, and let the client poll or use webhooks for the result. Increase timeout values in your reverse proxy if the operation genuinely needs more time. Optimize slow queries and database indexes to reduce processing time.
Quick Reference Table
Here's a condensed reference of every status code covered in this guide. Bookmark this for quick lookups during debugging sessions.
Code | Name | Meaning
------|-----------------------|------------------------------------------
200 | OK | Request succeeded, response body included
201 | Created | New resource created successfully
204 | No Content | Success, but no response body
301 | Moved Permanently | Resource permanently moved to new URL
302 | Found | Resource temporarily at a different URL
304 | Not Modified | Cached version is still valid
400 | Bad Request | Malformed syntax or invalid parameters
401 | Unauthorized | Authentication required or credentials invalid
403 | Forbidden | Authenticated but lacking permission
404 | Not Found | Resource does not exist at this URL
405 | Method Not Allowed | HTTP method not supported for this URL
409 | Conflict | Request conflicts with current server state
422 | Unprocessable Entity | Valid syntax but semantically invalid data
429 | Too Many Requests | Rate limit exceeded, slow down
500 | Internal Server Error | Unexpected server-side failure
502 | Bad Gateway | Proxy received invalid upstream response
503 | Service Unavailable | Server temporarily overloaded or in maintenance
504 | Gateway Timeout | Proxy timed out waiting for upstream responseConclusion
HTTP status codes are more than just numbers — they're a communication protocol between clients and servers. Using them correctly makes your APIs more predictable, your error messages more helpful, and your debugging sessions shorter. The key principles to remember: always use the most specific code that applies (201 over 200 for creation, 422 over 400 for validation failures), always include helpful error messages in the response body, and always handle errors gracefully on the client side with retry logic for transient failures.
When you encounter an unfamiliar status code in your logs or network inspector, come back to this guide. And if you're dealing with cryptic error responses from an API, try pasting the full error message into Pastefix's Error Decoder — it will break down what went wrong and suggest concrete fixes in seconds.
Try it yourself
Use Pastefix's Error Decoder to instantly analyze and understand your own data — no signup required.
Open Error Decoder→