404 Not Found
HTTP 404 is the most recognised error code on the web. Defined in RFC 9110, it tells the client that the server received the request but found no resource at the given URI. The response body is optional: many servers return an HTML page, while APIs typically return a JSON object with a message field. Consequently, clients should never assume a particular body format on a 404. The code applies whether the resource never existed, was deleted, or simply lives at a different path. It does not reveal which of those is true, and this deliberate vagueness is by design. Knowing the actual cause requires server-side log inspection rather than reading the status code alone.1
What is 404?
RFC 9110, Section 15.5.5. The origin server did not find a current representation for the target resource, or is not willing to disclose that one exists. The code covers missing pages, deleted records, and resources the server deliberately hides. A server may return 404 instead of 403 to avoid revealing that a protected resource exists at that path. 404 responses are cacheable by default under RFC 9110.2 The 404 status code only indicates that the resource is missing without indicating if this is temporary or permanent; if a resource is permanently removed, servers should send 410 Gone instead.3When the server should return 404
Returning 404 is appropriate when the URI maps to no known resource in your system, whether that resource never existed, was deleted, or simply lives at a different path on your server. Inside a REST API, a GET request to /users/9999 should return 404 if user 9999 does not exist, and the same rule applies to any lookup endpoint that references a record that cannot be found in your data store.
Nested resource paths
Nested resource paths follow the same rule: GET /posts/1/comments/999 returns 404 when comment 999 does not exist, regardless of whether post 1 is valid. For resources that are temporarily unavailable rather than permanently absent, 503 or 404 with a Retry-After header are both reasonable choices depending on whether the client should try the same URL again.
The distinction matters because 404 responses are cacheable by default under RFC 9110 and downstream caches may serve the stored response to subsequent callers, whereas 503 responses are not cached by default. Always check your caching configuration before returning 404 for anything ephemeral, and document the behaviour in your API specification so clients know what to expect.
404 vs 410 Gone
Choosing between 404 and 410 Gone affects search engine behaviour and client caching across your entire site, so the decision has consequences that go beyond a single failed request.4 A 410 Gone response explicitly states that the resource was permanently removed and will not return. Search engines deindex a 410 URL faster than a 404, making 410 the right choice after a deliberate deletion of a publicly indexed resource.
404 is appropriate when you are uncertain whether the resource ever existed, or when you might recreate it later. Many applications return 404 for everything and accept the slower deindexing. For high-traffic pages with many inbound links, the 410 distinction is worth the effort. RFC 9110 treats both as client errors, but only 410 signals finality to downstream systems.
Debugging a 404 response
When you encounter a 404, start with the URI itself and verify the exact path including trailing slashes, case sensitivity, and URL encoding before looking anywhere else. Path casing is a common source of 404s on Linux servers: /Products and /products are different paths on Linux but identical on macOS and Windows, so a mismatch between the client URL and the server route is often the first thing to rule out.
Framework and data layer checks
After confirming the path looks correct, check the routing table in your application framework, then inspect the data layer to confirm the record exists in the database. Your reverse proxy or CDN may also rewrite paths before they reach your application, producing 404s that only appear in production and never show up when you test the same URL against your local development server. Check server access logs to confirm what path the application actually received, and compare it against the route definitions in your framework to spot any mismatch.
Soft 404s and how search engines detect them
Soft 404s are the most common indexing mistake in web development. A soft 404 occurs when a page returns 200 OK but displays a "not found" message, a maintenance placeholder, or completely empty content. Google detects soft 404s by comparing the rendered content of a 200 response to what it expects from a meaningful page. Pages with unusually low word counts, no headings, or content matching known error phrase patterns are flagged and excluded from the index even though they returned 200.
You can identify soft 404s in your site by checking the Coverage report in Google Search Console. Under "Excluded," the reason "Soft 404" appears alongside the affected URLs. Returning the correct status code is the only reliable fix: a missing-resource page should return 404, a deleted page should return 410 Gone, and a maintenance holding page should return 503 Service Unavailable with a Retry-After header.
Returning 200 for a maintenance page has a compounding effect over time that goes beyond a single page's traffic. Google indexes the maintenance content and replaces the real page content in the search index. After the maintenance ends and the real content returns, the crawler must revisit and reindex the correct page. This delay can suppress rankings for hours or days after the maintenance window closes, depending on the page's crawl priority and your domain's crawl budget allocation.
Customising 404 error responses for REST APIs
A well-designed 404 response serves two audiences: the browser user who arrives at a broken link, and the API client that receives an unexpected 404 on a programmatic request. For browser users, a 404 page should include a search field, links to popular sections, and a clear explanation that the requested page was not found. For API clients, the 404 body should follow your error response contract: a JSON object with a machine-readable error code and a message, consistent with the format your other 4xx responses use.
REST APIs that return an HTML 404 error page when a client expects JSON force the client to detect the response format before parsing the body, which adds unnecessary complexity to every error path in the calling code. Add a Content-Type check in your error middleware to ensure 404 responses always include a JSON body for API routes. A client parsing an error body that unexpectedly contains HTML will throw a parse error, masking the original 404 and complicating debugging in ways that are hard to trace back to the response format mismatch.
Including the request path in the 404 body
Providing the requested URI in the 404 response body lets the client log exactly which resource was missing, which is valuable when the same client calls multiple endpoints in a single workflow. Include the full request path in the error body as a "path" or "instance" field; this is especially useful in microservices where a 404 may propagate through multiple services before reaching the caller. An "instance" field aligns with RFC 7807 Problem Details, making the 404 body compatible with clients that already parse Problem Details from other APIs.5
Try in the tool
Open the HTTP Status Code Reference tool pre-filled to 404 to verify it or try a different one.
Check 404 in the tool →- 1.
R. Fielding, Ed., M. Nottingham, Ed., and J. Reschke, Ed., "HTTP Semantics," RFC 9110, IETF, June 2022. https://www.rfc-editor.org/rfc/rfc9110.txt
- 2.
R. Fielding, Ed., M. Nottingham, Ed., and J. Reschke, Ed., "HTTP Caching," RFC 9111, IETF, June 2022. https://datatracker.ietf.org/doc/html/rfc9111
- 3.
Mozilla Developer Network, "404 Not Found," developer.mozilla.org, accessed June 2026. https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/404
- 4.
Google, "Do 404 errors hurt my site?," developers.google.com, May 2011. https://developers.google.com/search/blog/2011/05/do-404s-hurt-my-site
- 5.
M. Nottingham and R. Wilde, "Problem Details for HTTP APIs," RFC 7807, IETF, March 2016. https://www.rfc-editor.org/rfc/rfc7807.txt
404 Not Found means the resource is absent but may exist in the future. 410 Gone means the resource was permanently removed and will not return. Search engines deindex a 410 URL faster than a 404. Use 404 for missing resources and 410 for deliberate, permanent deletions of publicly indexed pages.
This is a security decision. Returning 403 confirms the resource exists; returning 404 conceals it. Use 403 when it does not matter if users know the resource is there. Use 404 when you want to prevent enumeration of protected resources, such as private user profiles.
Yes. RFC 9110 defines 404 as cacheable by default, so caches may store and serve it for subsequent identical requests. Add Cache-Control: no-store if you want to prevent caching, especially for resources you plan to create in the near future.
File-system case sensitivity is the most common cause. Linux treats /Users and /users as different paths; macOS and Windows do not. Also verify that your CDN or reverse proxy does not rewrite paths before they reach your app, and confirm the deployment includes all expected static files.
Yes. A structurally valid request returns 404 when the referenced record does not exist in the database. A GET /orders/12345 with correct headers returns 404 if order 12345 was deleted. CapyToolkit lets you inspect the full request and response headers for any URL, so you can confirm the status code and the body format your server returned without opening a separate debugging tool.
500 Internal Server Error
500 Internal Server Error is the generic server-side catch-all. Defined in RFC 9110, Section 15.6.1, it tells the client the server encountered an unexpected condition that prevented it from fulfilling the request.1 Unlike 4xx codes, a 500 places no obligation on the client to change the request: the same request would succeed if the server-side failure were resolved. Consequently, the client can do little other than retry or report the error. The code covers unhandled exceptions, database connection failures, out-of-memory events, and any other condition the application did not anticipate. Production servers suppress error details in the response body to prevent information leakage, so diagnosing a 500 requires access to server-side logs. Monitoring and alerting on 500 rates is standard practice because a spike almost always indicates a deployment regression, a configuration error, or a failing upstream dependency.
What is 500?
RFC 9110, Section 15.6.1. The server encountered an unexpected condition that prevented it from fulfilling the request.1 The definition is deliberately broad: any unhandled exception, fatal runtime error, or system-level failure maps to this code. Unlike 4xx codes, 500 places no obligation on the client to change the request. Retrying may succeed if the failure is transient, though 503 with a Retry-After header signals transience more explicitly. Servers should log the full error details internally.When the server should return 500
Returning 500 is appropriate when the application encounters an unhandled error that is not caused by the client request. Database connection timeouts, out-of-memory conditions, unhandled exceptions, and filesystem permission errors are all valid 500 triggers. Application frameworks typically return 500 automatically when an unhandled exception reaches the top of the call stack, so explicit 500 responses from application code are relatively rare in practice.2
Some teams intentionally catch all exceptions at a top-level error handler and return 500 for any unanticipated condition, which is sound as long as the actual error is logged with enough context to reproduce it. The risk of this approach is that it can mask the true nature of failures: a network timeout and a null pointer exception both surface as the same generic 500, making it harder to diagnose issues in production without examining the logs.
500 should not be used for conditions the application explicitly handles. A missing record warrants 404, a validation failure warrants 422, and a service that is temporarily overloaded warrants 503. Choosing the correct code for each failure mode gives your clients actionable information about what went wrong and whether retrying the same request is likely to succeed.
When to return 503 instead of 500
Return 503 Service Unavailable with a Retry-After header when you know the failure is transient and the client should retry the same URL after a delay. This applies to planned maintenance windows, capacity-related refusals, and upstream service outages that your application detects before attempting to process the request. Using 503 instead of 500 in these cases prevents clients from interpreting a temporary condition as a permanent application error and gives them a clear signal about when to try again.
500 vs 502 vs 503
Three 5xx codes cover most server-side failures, and the distinctions matter for debugging and monitoring because each code points to a different layer of your system. Choosing the correct code ensures that your monitoring dashboards, alerting rules, and runbooks all reflect the true source of the failure rather than lumping every server-side problem into a single bucket.
Reading the code as a fault signal
A 500 Internal Server Error originates from your application code: the server received the request, ran the application, and something inside broke.3 A 502 Bad Gateway originates from a proxy or load balancer: the upstream server returned an invalid response. A 503 Service Unavailable originates from the server intentionally refusing requests because capacity is full or maintenance is in progress.
A 500 spike points to application logs. A 502 spike points to upstream service health. A 503 spike points to load and capacity metrics, so the on-call engineer can triage the issue without guessing which subsystem to investigate first. Mixing these codes in a single "server error" dashboard obscures which layer is failing. Instrument each code separately in your monitoring system.
Debugging 500 responses
Debugging a 500 response begins at server logs, not the HTTP client. Most production servers suppress internal error details to prevent information leakage, so the response body rarely helps. Look for the full stack trace and error message in your application log immediately after the failed request timestamp, and include the request ID so you can correlate the response to the log entry.
Timing the error against recent deployments is the second step. A spike that starts right after a release indicates a code regression. A gradual increase suggests resource exhaustion or a degrading upstream dependency. Structured logging with request IDs lets you correlate the exact 500 response a client received with the specific log entry on the server, which is especially valuable in distributed systems where a single user request touches multiple services.
Add an alert on your 500 error rate as a percentage of total requests so transient spikes do not go unnoticed. Reproduce the failing request in a staging environment before changing production code, and verify the fix by monitoring the 500 rate for at least one full traffic cycle after the deployment.
Structured logging and correlation IDs for 500 responses
Unstructured 500 error logs are the most common cause of slow incident resolution. When a 500 response reaches a client, the only path to diagnosis is the server log entry recorded at the same moment. A log entry that contains only a stack trace with no request context forces the on-call engineer to search for the request manually, correlate it to the stack trace, and piece together the reproduction path from incomplete data.
Structured JSON logging solves this by emitting a single log event per request that contains all the context needed for diagnosis: the HTTP method, the request path, the response status code, the error message, the stack trace, and a unique trace ID.4 The trace ID should be generated at the request entry point and propagated through all downstream calls, so that a single trace ID in the 500 response body links to a complete picture of what happened during that request.
Choosing a trace ID format and propagation strategy
OpenTelemetry defines a standard W3C Trace Context header (traceparent) that carries a 128-bit trace ID and a 64-bit span ID through all downstream HTTP calls.5 Including the trace ID in the 500 response body (not the full trace, just the ID) gives the client a reference string they can share with your support team without exposing internal stack details. Most observability platforms accept the traceparent format and link the trace ID to the full distributed trace automatically.
Try in the tool
Open the HTTP Status Code Reference tool pre-filled to 500 to verify it or try a different one.
Check 500 in the tool →- 1.
R. Fielding, Ed., M. Nottingham, Ed., and J. Reschke, Ed., "HTTP Semantics," RFC 9110, IETF, June 2022. https://www.rfc-editor.org/rfc/rfc9110.txt
- 2.
Django, "Built-in Views: The 500 (Server Error) view," docs.djangoproject.com, accessed June 2026. https://docs.djangoproject.com/en/5.2/ref/views/
- 3.
MDN, "502 Bad Gateway," developer.mozilla.org, accessed June 2026. https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Status/502
- 4.
Microsoft, "Observability Patterns — .NET (Cloud-Native)," learn.microsoft.com, accessed June 2026. https://learn.microsoft.com/en-us/dotnet/architecture/cloud-native/observability-patterns
- 5.
W3C, "Trace Context," W3C Recommendation, w3.org, November 2021. https://www.w3.org/TR/trace-context/
A 500 indicates the server encountered an unexpected condition in its code or infrastructure. Common causes include unhandled exceptions, failed database connections, memory exhaustion, misconfigured environment variables, and filesystem permission errors. The client request itself is not the cause: the same request would succeed without the server-side failure.
Sometimes. A 500 caused by a transient condition like a momentary database timeout may succeed on retry. However, the code gives no indication of whether the failure is transient or permanent. For explicitly retryable conditions, use 503 with a Retry-After header. Retry a 500 only if the operation is idempotent and the failure is likely transient.
In production, no. Exposing stack traces, database schema details, or internal paths in a 500 response body is a security risk. Log the full error internally and return a minimal JSON body with a generic message. In development or staging, verbose error details speed up debugging, but never expose them in production.
A 500 means the server encountered an unexpected error while processing the request. A 503 means the server is intentionally refusing to process requests because it is overloaded or in maintenance mode. Use 503 with a Retry-After header when the service will recover; use 500 for unhandled application failures.
Set an alert on your 500 error rate as a percentage of total requests, not on absolute count alone. A rate spike after a deployment is a clear signal of a code regression. CapyToolkit does not store or upload any data, so you can safely paste a failing URL into the built-in network tool to inspect the exact response headers and body your server returned without exposing your infrastructure to a third-party service.
403 Forbidden
403 Forbidden means the server understood the request but refuses to fulfill it. Defined in RFC 9110, Section 15.5.4, it signals that the client's identity is known but the server has decided not to grant access to the requested resource.1 Unlike 401, which invites the client to authenticate, 403 indicates that authentication would not help: the credential is present or irrelevant, and the decision is authorisation-based.2 Consequently, returning 403 to a client tells it clearly that retrying with different credentials for the same identity will not succeed. The code is appropriate for role-based access control failures, IP allowlist rejections, and resource ownership checks. Its interaction with 404 creates a common security design choice: returning 404 instead of 403 prevents an attacker from confirming that a protected resource exists at that path.
What is 403?
RFC 9110, Section 15.5.4. The server understood the request but refuses to authorize it. Unlike 401, sending authentication will not help and the request should not be repeated. The server may disclose why access is forbidden in the response body, but is not required to. A server may also return 403 to conceal the existence of a protected resource. 403 responses are not cacheable by default under RFC 9110.3403 vs 404: the security tradeoff
Choosing between 403 and 404 for a protected resource is a security design decision with meaningful consequences for both your users and your attack surface. Returning 403 confirms the resource exists and the client lacks permission. Returning 404 conceals whether the resource exists at all, which is the safer choice when you do not want to reveal the structure of your application to unauthenticated or underprivileged clients.2
For public-facing APIs where resource enumeration is a threat, returning 404 instead of 403 prevents attackers from discovering which resource IDs or paths exist. Private user profile pages, internal admin endpoints, and multi-tenant data all benefit from this pattern. For developer-facing APIs where permission errors need to be debugged quickly, 403 with a clear message is more helpful than a silent 404 that gives no indication of whether the resource is missing or merely inaccessible.
Choose one convention per API surface and apply it consistently across every endpoint in that surface so that clients can rely on a predictable error contract. Mixing 403 and 404 for the same type of protected resource makes permission debugging harder than it needs to be and creates an inconsistent experience for every client integrating with your API, especially when the same team maintains multiple services that share an authentication layer.
Role-based access control and 403
Inside role-based access control systems, 403 is the correct response when an authenticated user attempts an action their role does not permit.4 A user with a read-only role attempting a DELETE request should receive 403, not 401. Resource-level permission failures follow the same pattern: a user who can read their own orders but not another user's should receive 403 on the cross-account request.
Logging and audit trails
Many frameworks implement RBAC via middleware that inspects the authenticated identity and the requested resource before the route handler runs, returning 403 before any business logic executes. Log the identity and the denied permission in your server access logs when returning 403: this creates an audit trail for security review and helps diagnose misconfigured role assignments. Never expose internal role names or policy details in the 403 response body.
When to use 403 vs 401
The 401 vs 403 decision maps to authentication versus authorisation. Return 401 when the request lacks credentials or the credentials are invalid, and when providing correct credentials would plausibly grant access. Return 403 when credentials are present and valid but the identity lacks the required permission for this specific resource or action.
A third option exists: returning 404 when you want to conceal whether a resource exists for an authenticated user who lacks permission. A request with a valid JWT that lacks the required scope receives 403, not 401. A request with no JWT or an expired JWT receives 401. Applying this distinction correctly prevents clients from entering authentication retry loops when the real problem is a permission configuration error.
Common 403 misconfiguration patterns
The most frequent cause of unexpected 403 responses in production is a mismatch between the permissions your middleware checks and the permissions your route handlers assume. Another common pattern is a CORS preflight request that reaches your authorization middleware before the framework handles the OPTIONS method, resulting in a 403 on a request the browser expects to succeed. Handling OPTIONS requests before your authentication middleware prevents this condition and keeps your cross-origin flows working as expected.
Logging 403 responses for security auditing
Every 403 response is a potential security event worth recording. A user encountering a single 403 is likely a normal permissions issue. A user or IP address generating dozens of 403 responses against resource IDs or admin paths in a short window is probing your authorization layer. Without structured logging that captures the authenticated identity, the requested path, and the HTTP method on every 403, this pattern is invisible until damage occurs.
Structure your 403 log events to include: the authenticated user ID or session ID, the requested URL path, the HTTP method, the timestamp, and the originating IP address. Storing these fields in a searchable format and shipping them to an observability platform allows you to write queries that detect enumeration attacks before they succeed. A rate-based alert on 403 events per user per hour catches automated probing that manual log review would miss.
Avoiding information leakage in 403 response bodies
Your 403 response body should not name the specific permission the request lacked, the internal role the user is missing, or the policy that blocked the request.5 Each of these details narrows the attacker's search space. A body containing "ADMIN_ROLE_REQUIRED" tells a probing client exactly which role to target through other attack vectors. A body stating "You do not have permission to perform this action" provides the same information to a legitimate developer without the specificity that aids an attacker.
Regular auditing of 403 response bodies in your staging environment catches accidental information leakage before it reaches production. You can script a test that sends authenticated requests to protected endpoints with insufficient permissions and verifies that the response body contains only generic messaging. This check belongs in your CI pipeline alongside other security tests, because a single deployment that accidentally includes a detailed error message can expose your authorization model to anyone who triggers a 403.
Try in the tool
Open the HTTP Status Code Reference tool pre-filled to 403 to verify it or try a different one.
Check 403 in the tool →- 1.
R. Fielding, Ed., M. Nottingham, Ed., and J. Reschke, Ed., "HTTP Semantics," RFC 9110, IETF, June 2022. https://www.rfc-editor.org/rfc/rfc9110.txt
- 2.
Mozilla Developer Network, "403 Forbidden," developer.mozilla.org, accessed June 2026. https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Status/403
- 3.
Mozilla Developer Network, "Cacheable," developer.mozilla.org, accessed June 2026. https://developer.mozilla.org/en-US/docs/Glossary/Cacheable
- 4.
OWASP Foundation, "Authorization Cheat Sheet," cheatsheetseries.owasp.org, 2024. https://cheatsheetseries.owasp.org/cheatsheets/Authorization_Cheat_Sheet.html
- 5.
OWASP Foundation, "Error Handling Cheat Sheet," cheatsheetseries.owasp.org, 2024. https://cheatsheetseries.owasp.org/cheatsheets/Error_Handling_Cheat_Sheet.html
401 means the request lacks valid credentials and the client should authenticate. 403 means credentials are present (or authentication is not the issue) but the server refuses to grant access. A 401 invites the client to log in; a 403 tells the client that logging in with the same identity will not help.
It depends on your security requirements. Returning 403 confirms the resource exists; returning 404 conceals it. For private resources where enumeration is a risk, 404 is the safer choice. For developer APIs where permission errors need to be debugged, 403 with a clear message is more useful. Pick one convention and apply it consistently.
Not by default. RFC 9110 does not list 403 as cacheable. Caching a 403 would prevent a client from accessing a resource after its permissions change. Most servers set Cache-Control: no-store on 403 responses to ensure the access check runs fresh on every request.
Yes. IP allowlist rejections are a server-side access control decision that does not depend on authentication state. The server knows the source IP and has decided to deny the request. 403 is correct for this case. Some servers return 403 at the firewall or proxy layer before the request even reaches the application.
Only in contexts where it is safe to do so. For internal APIs where all clients are trusted, including a reason string in the body helps debugging. CapyToolkit does not collect or store any data you paste into its tools, so you can decode and inspect tokens, headers, and error bodies safely when troubleshooting 403 failures in development. For public-facing APIs, avoid leaking internal role names, policy details, or resource paths in the 403 body.
429 Too Many Requests
429 Too Many Requests signals a rate limit has been hit. Defined in RFC 6585, Section 4, it tells the client it has sent too many requests in a given time window and the server is throttling it.1 The server may include a Retry-After header indicating how long the client should wait before trying again.2 Consequently, well-behaved clients inspect this header and back off for the specified duration rather than hammering the server with retries. The code is used by APIs, web applications, and CDNs to protect resources from overuse, whether by a single user, an IP address, or an API key. Understanding how rate limiting is implemented on the server side helps clients design appropriate retry and backoff strategies. A 429 is not an error in the traditional sense: it is the server enforcing a deliberate policy.
What is 429?
RFC 6585, Section 4. The user has sent too many requests in a given amount of time. The server may include a Retry-After header indicating how long the client should wait before making a new request. The code applies to all rate-limiting scenarios regardless of whether the limit is per IP, per user, per API key, or per application. 429 is not listed as cacheable by default under RFC 9110.3The Retry-After header
When a server returns 429, including a Retry-After response header is strongly recommended by RFC 6585. The header takes either a number of seconds the client should wait or an HTTP-date after which the client may retry. Retry-After: 30 tells the client to wait 30 seconds before retrying. Many rate limiting libraries populate this header automatically based on when the current window resets.
Implementing cooperative backoff
Clients that honour Retry-After implement a cooperative backoff: they reduce load on the server during congestion rather than amplifying it with immediate retries. Many client libraries and HTTP frameworks do not inspect Retry-After by default, so developers must explicitly read the header and implement the wait. Without it, the client falls back to exponential backoff, which achieves a similar outcome but less efficiently than following the server's explicit instruction.
Token bucket vs leaky bucket rate limiting
Two algorithms dominate API rate limiting implementations in production today, and knowing which one the server uses helps clients predict when 429s will occur and how to structure their retry logic accordingly.4 The choice of algorithm shapes not only how clients behave under load but also how your server capacity planning accounts for bursty traffic patterns.
Token bucket allows bursting: the client accumulates tokens over time and spends them on requests, so a client that was idle for several minutes can send a burst up to the bucket capacity before hitting 429. Leaky bucket enforces a steady rate: requests are processed at a fixed rate regardless of how many accumulate, so even a single burst above the rate triggers throttling. In practice, many APIs combine token bucket with a sustained rate limit to allow short bursts while still enforcing a long-term average.
Fixed-window rate limiting resets the counter at the start of each window, which means a burst at the end of one window and the start of the next can allow twice the nominal rate momentarily. Sliding-window rate limiting averages the request count over a rolling period and prevents this pattern. API documentation should specify which algorithm and window type the server uses so that clients can model their request patterns accurately.
Client-side 429 handling
Handling 429 responses correctly on the client side prevents cascading failures and respects server capacity when a single client might otherwise overwhelm a shared resource. The first step is to check for a Retry-After header and wait the specified duration before retrying. Without this header, implement exponential backoff with jitter: double the wait time on each consecutive 429 starting from a small base interval, and add a random jitter value to prevent multiple clients from retrying simultaneously and creating a thundering herd. Distinguish between rate limits that affect a single user and those that affect the entire application, because the retry strategy for a per-key limit differs from the backoff you need when an IP-level limit at a CDN is blocking all users behind that address.
Add circuit-breaker logic to stop retrying after a configurable number of consecutive 429s and surface the error to the user rather than silently retrying indefinitely. Log the rate limit headers on every 429 response to diagnose which limit was hit, and consider adding a metric per limit type so your team can track which customers are hitting the ceiling most frequently.
Choosing the right backoff strategy for your use case
Exponential backoff with jitter works well for single-client applications where you control the entire request lifecycle, but distributed systems often need a more coordinated approach such as a shared rate limiter that tracks consumption across all instances. If your application runs on multiple servers, a local circuit breaker alone will not prevent collective overuse because each instance independently estimates the remaining quota. Pairing client-side backoff with a centralized rate limit counter gives you the best of both worlds: fast local decisions backed by accurate global state.
Rate limit headers and proactive client throttling
Informational rate limit headers let clients pace their requests proactively rather than discovering the limit by hitting 429. Three headers have become the de facto standard across major APIs even though none are defined in a finalized RFC: X-RateLimit-Limit reports the total request quota for the current window, X-RateLimit-Remaining reports how many requests remain, and X-RateLimit-Reset reports the Unix timestamp at which the window resets.5 GitHub, Stripe, and Twilio all include these headers on every response, not just on 429 responses.
Including these headers on every 200 response allows well-behaved clients to read X-RateLimit-Remaining before making the next request and pause if it is approaching zero. This prevents the client from ever triggering 429 at all, which is better for both the client and the server than handling the error reactively. A client library that inspects these headers on each response can implement automatic throttling without any developer configuration.
The RateLimit-Policy header from IETF draft
IETF draft draft-ietf-httpapi-ratelimit-headers standardises rate limit header semantics under the names RateLimit-Limit, RateLimit-Remaining, and RateLimit-Reset (without the X- prefix).6 The draft adds RateLimit-Policy for documenting multiple rate limit policies simultaneously, such as per-IP and per-user limits enforced at the same time. Adopting the draft names positions your API for compatibility with API gateways and client libraries that will adopt the standard once the draft is published as an RFC.
Testing your API against the draft header format before it becomes an RFC gives you a migration path with zero downtime. You can emit both the X- prefixed headers and the new RateLimit- headers simultaneously during the transition period, allowing clients that support either format to function correctly. Once the standard is finalised, you can drop the X- headers in a minor version bump without breaking existing integrations.
Try in the tool
Open the HTTP Status Code Reference tool pre-filled to 429 to verify it or try a different one.
Check 429 in the tool →- 1.
M. Nottingham and R. Tarreau, "Additional HTTP Status Codes," RFC 6585, IETF, April 2012. https://www.rfc-editor.org/rfc/rfc6585.txt
- 2.
Mozilla Developer Network, "429 Too Many Requests," developer.mozilla.org, accessed June 2026. https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Status/429
- 3.
Mozilla Developer Network, "Cacheable," developer.mozilla.org, accessed June 2026. https://developer.mozilla.org/en-US/docs/Glossary/Cacheable
- 4.
"Token bucket," Wikipedia, accessed June 2026. https://en.wikipedia.org/wiki/Token_bucket
- 5.
GitHub, "Rate limits for the REST API," docs.github.com, accessed June 2026. https://docs.github.com/en/rest/using-the-rest-api/rate-limits-for-the-rest-api
- 6.
R. Polli, A. Martinez, and D. Miller, "RateLimit header fields for HTTP," draft-ietf-httpapi-ratelimit-headers-11, IETF, May 2026. https://datatracker.ietf.org/doc/html/draft-ietf-httpapi-ratelimit-headers-11
Check the Retry-After header first. If present, wait the specified number of seconds before retrying. If absent, use exponential backoff with jitter: start with a short wait and double it on each consecutive 429, adding a random offset. Never retry immediately on a 429: immediate retries amplify load and typically result in more 429 responses.
429 is the correct code for client-level rate limiting: the server is throttling this specific client because of its request volume. 503 Service Unavailable means the server is globally overloaded or in maintenance. Use 429 when the server is functioning normally but enforcing a quota; use 503 when the service cannot handle any requests regardless of the client.
Both formats are valid under RFC 6585. A number of seconds (e.g., Retry-After: 60) is simpler for clients to parse and avoids clock synchronisation issues between client and server. An HTTP-date is useful when the rate limit resets at a fixed wall-clock time, such as the top of the next minute.
429 is not cacheable by default under RFC 9110. Caching a 429 would prevent subsequent requests from reaching the server after the rate limit window resets, blocking clients that should now be allowed through. Set Cache-Control: no-store on 429 responses to ensure each request is evaluated against the current rate limit state.
Token bucket is a good default for most APIs: it allows controlled bursting while enforcing an average rate. Use sliding-window rate limiting when you need to prevent double-bursting at window boundaries. CapyToolkit allows you to test rate limit headers against a URL so you can verify which algorithm and window type your server uses before committing to a client-side strategy. Leaky bucket is appropriate when you need a strictly steady request rate, such as for upstream dependencies that cannot absorb any burst at all.
502 Bad Gateway
502 Bad Gateway indicates an upstream server returned an invalid response. Defined in RFC 9110, Section 15.6.3, it is produced by a proxy or gateway that received a response from an upstream server that was syntactically incorrect, empty, or could not be parsed as a valid HTTP response.1 The 502 originates at the proxy layer, not inside the application code. Consequently, diagnosing a 502 requires inspecting the proxy or load balancer logs rather than the application logs. Common upstream failures that produce 502s include a crashed application process returning no response, a response that does not conform to the HTTP protocol, or a connection that was forcibly closed before the response headers were sent. Understanding 502 versus 503 and 504 points debugging efforts at the right layer of the stack immediately.
What is 502?
RFC 9110, Section 15.6.3. The server, while acting as a gateway or proxy, received an invalid response from an upstream server it accessed while attempting to fulfill the request. The 502 is produced by the intermediary, not the origin server. It indicates the upstream returned something the proxy could not interpret as a valid HTTP response, or the connection to the upstream failed after the request was forwarded. 502 is not cacheable by default under RFC 9110.2Debugging 502 at the reverse proxy
Debugging a 502 starts at the reverse proxy or load balancer access logs, not the application. The proxy log typically records why it returned 502: a connection refused error means the upstream application is not listening on the expected port, a connection reset means the upstream closed the connection unexpectedly, and a read timeout means the upstream did not send response headers within the proxy's configured timeout.
Vendor-specific annotations
The proxy may annotate the 502 with a vendor-specific code. Nginx logs "upstream sent invalid header" or "connect() failed". AWS Application Load Balancer logs the target group health check status. Correlate the timestamp of the 502 with application deployment events: a newly deployed version that crashes on startup produces a burst of 502s from the proxy before the load balancer marks the target unhealthy and stops routing to it.
502 vs 503 vs 504
Three gateway error codes cover most proxy-layer failures, and distinguishing them accelerates diagnosis because each code points to a different root cause and a different team to investigate the incident. Conflating these codes in a single monitoring dashboard is one of the most common mistakes teams make when instrumenting proxy-layer errors.
502 Bad Gateway means the proxy received an invalid or empty response from the upstream: the upstream was reachable but responded incorrectly. 503 Service Unavailable means the proxy could not reach any healthy upstream at all, or the upstream explicitly returned 503 itself. 504 Gateway Timeout means the upstream was reachable but did not respond within the proxy's timeout window.3
A 502 spike points to an application crash or protocol error at the upstream. A 503 spike points to all upstreams being unhealthy or the service being in maintenance. A 504 spike points to the upstream being too slow, often due to database query latency or external API timeouts. Monitoring each code with separate dashboards prevents conflating these three distinct failure modes and ensures the right team is paged for each incident.
Common causes of 502
Several specific conditions reliably produce 502 responses in production environments, and recognising the pattern in your proxy logs helps you identify which one is affecting you without restarting services blindly. An application process that crashes immediately after starting returns no response headers, causing the proxy to log "upstream sent no valid HTTP response" and return 502 to the client.
A too-short keepalive timeout on the upstream application causes the connection to be closed just as the proxy sends a request on a reused connection, producing a 502 on that request. Large response headers that exceed the proxy's buffer size cause the proxy to fail parsing the response and return 502 as well.4 Both conditions are intermittent by nature, so a single successful request does not rule them out.
A Python or Ruby application that raises an unhandled exception before writing response headers also produces 502 rather than 500, because the proxy never received a valid response to forward. Check both the application restart logs and the proxy error logs simultaneously when diagnosing a 502 to identify which of these conditions is occurring.
Preventing 502s with upstream health checks
Configuring active upstream health checks on your load balancer or reverse proxy lets the proxy detect unhealthy upstreams before it forwards client requests to them, which prevents many 502 responses from ever reaching your users. Most modern proxies support HTTP health checks that poll a dedicated endpoint on each upstream at a configurable interval and mark the backend unhealthy after a configurable number of consecutive failures. Pairing health checks with the proxy_next_upstream retry logic described below gives you two layers of defense: the health check avoids sending traffic to a known-bad backend, and the retry logic recovers from transient connection races that slip past the health check.
Keepalive timeout mismatches and 502 prevention in nginx
Keepalive timeout mismatches are the most common cause of intermittent 502 responses in nginx deployments. When nginx reuses a persistent connection to forward a request to the upstream application, the upstream may have already closed that connection.5 The upstream's keepalive timeout setting must be higher than nginx's keepalive_timeout directive to ensure connections remain alive from the upstream's perspective when nginx reuses them.
For Node.js applications behind nginx, the Node.js server's default keepalive timeout is 5 seconds.6 If nginx's keepalive_timeout is 65 seconds, nginx may try to reuse a connection that Node.js has already closed, producing a 502 "upstream sent no valid HTTP response." Setting Node.js's server.keepAliveTimeout to 75 seconds (slightly higher than nginx's directive) prevents this race condition. The fix requires coordinating both the nginx configuration and the application server configuration simultaneously.
Using proxy_next_upstream to recover from connection race conditions
Adding proxy_next_upstream error timeout http_502 in your nginx upstream block tells nginx to retry the request on a fresh connection when it receives a 502 from the upstream.4 This configuration recovers silently from keepalive race conditions without any change to the upstream application. Limit retries with proxy_next_upstream_tries 2 to prevent infinite retry loops, and confirm the upstream handler is idempotent before enabling retry for POST requests to avoid duplicate processing.
Testing the keepalive configuration under load before deploying to production catches timeout mismatches that only appear under concurrent traffic. You can simulate the race condition with a load testing tool that holds persistent connections while the upstream server cycles through restart phases, then verify that nginx's retry logic absorbs the transient 502s without surfacing them to clients. This validation step is especially important when you change either the nginx or application server keepalive settings, as the interaction between the two is the source of the failure mode.
Try in the tool
Open the HTTP Status Code Reference tool pre-filled to 502 to verify it or try a different one.
Check 502 in the tool →- 1.
R. Fielding, Ed., M. Nottingham, Ed., and J. Reschke, Ed., "HTTP Semantics," RFC 9110, IETF, June 2022. https://www.rfc-editor.org/rfc/rfc9110.txt
- 2.
Mozilla Developer Network, "Cacheable," developer.mozilla.org, accessed June 2026. https://developer.mozilla.org/en-US/docs/Glossary/Cacheable
- 3.
Mozilla Developer Network, "502 Bad Gateway," developer.mozilla.org, accessed June 2026. https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Status/502
- 4.
Nginx, "Module ngx_http_proxy_module," nginx.org, accessed June 2026. https://nginx.org/en/docs/http/ngx_http_proxy_module.html
- 5.
Nginx, "Module ngx_http_upstream_module," nginx.org, accessed August 2026. https://nginx.org/en/docs/http/ngx_http_upstream_module.html
- 6.
Node.js, "HTTP," nodejs.org, accessed August 2026. https://nodejs.org/api/http.html#serverkeepalivetimeout
A 502 is generated by a proxy, load balancer, or gateway, not by the application itself. The proxy received an invalid or empty response from the upstream server it was trying to contact. To debug a 502, inspect the proxy logs, not the application logs: the proxy records the specific reason it could not parse or forward the upstream response.
502 Bad Gateway means the upstream returned an invalid or malformed response. 504 Gateway Timeout means the upstream was reachable but did not respond within the proxy's timeout window. Both are proxy-layer errors, but 502 points to a protocol or crash issue at the upstream while 504 points to a latency or performance issue.
Yes. If the application crashes or exits before sending any response headers, the proxy receives no valid HTTP response from the upstream and returns 502 to the client. The 500 would only appear if the application managed to write a response with status 500 before crashing. A 502 with "upstream sent no valid HTTP response" in nginx logs is a classic crash symptom.
Set the upstream application's keepalive timeout to a value higher than nginx's keepalive_timeout directive. When the upstream closes a connection just as nginx reuses it, nginx returns 502. Adding proxy_next_upstream error timeout http_502; in nginx config tells nginx to retry the request on a fresh connection, resolving the transient 502 without application changes.
Yes, for idempotent requests. A 502 is typically a transient proxy-layer failure: the next request may succeed if the upstream has recovered. Add retry logic with exponential backoff for GET, PUT, and DELETE requests. CapyToolkit allows you to inspect the exact response your proxy returned without storing any data, so you can confirm whether the body contains a vendor-specific error annotation before deciding whether a retry is appropriate. Avoid automatically retrying POST requests that are not idempotent.
200 OK
200 OK is the baseline success response. Defined in RFC 9110, Section 15.3.1, it indicates that the request succeeded and the server is returning the requested content in the response body.1 The meaning of success depends on the HTTP method: for GET it means the resource was found and is in the body; for POST it means the action was accepted and the result is in the body; for PUT and PATCH it means the resource was updated. Consequently, 200 is not the only success code, and using it for every successful response is a common anti-pattern that discards useful semantic information. Understanding when 201 Created, 204 No Content, or 202 Accepted is more appropriate than 200 makes API responses more informative for clients, CDNs, and HTTP-aware infrastructure.
What is 200?
RFC 9110, Section 15.3.1. The request succeeded. The content of the response depends on the HTTP method: GET returns the requested resource, HEAD returns the headers of the GET response without the body, POST returns the result of the action, and OPTIONS returns the list of communication options. 200 responses are cacheable by default when the appropriate Cache-Control or Expires headers are present.2 The response typically includes a body.200 vs 201 vs 204
Inside the 2xx class, three codes handle the most common success scenarios in REST API design, and choosing the right one makes your API more self-documenting for every client that integrates with it. Use 200 OK for successful GET, PUT, and PATCH responses that include a body. Use 201 Created after a POST or PUT that produces a new resource at a new URI: the response should include a Location header pointing to the new resource. Use 204 No Content for a DELETE that succeeds with no body, or for a PUT or PATCH where you choose not to echo back the updated resource.3
Why 200 is not always correct
Returning 200 for a resource creation (where 201 is correct) misleads clients that inspect the status code to determine whether a new resource was created, and it also prevents HTTP-aware tooling from following the Location header automatically on behalf of the caller. Always include the Location header on 201 responses so clients do not need a separate GET to discover the new resource URI. The same reasoning applies to asynchronous operations: returning 200 for work that has been queued but not yet completed forces the client to treat a pending operation as a finished success, which is why 202 Accepted exists specifically for this case.
Caching with 200 responses
A 200 OK response is cacheable when the response includes Cache-Control or Expires headers that permit caching. Without these headers, a 200 response from a GET request may be heuristically cached by the browser based on the Last-Modified date, per RFC 9111.4 API responses that should not be cached must include Cache-Control: no-store or Cache-Control: no-cache, must-revalidate.
Cache keys for GET requests include the full request URI plus any Vary headers the server specifies. An API that varies by Authorization header must include Vary: Authorization to prevent cached responses from one user being served to another. CDNs typically do not cache responses with Authorization headers by default, so this Vary setting matters most for unauthenticated public endpoints.
Common misuses of 200
Several widespread patterns misuse 200 in ways that cause operational problems. Returning 200 with an error body is the most damaging: a response of HTTP/1.1 200 OK with {"error": "User not found"} in the body defeats HTTP-aware infrastructure.5 CDNs, load balancers, and API gateways make routing, retry, and caching decisions based on the status code, not the body.
Returning 200 for an asynchronous operation that has not yet completed misleads the client into thinking the work is done, and it prevents the client from distinguishing between a successful synchronous result and a job that may still fail on the server side. 202 Accepted is the correct code for accepted-but-not-yet-processed operations because it signals to the client that it should check a status endpoint for the final outcome rather than assuming the request succeeded.
Audit your API for any endpoints that return 200 for conditions that would be better represented by a 4xx or 5xx code, especially in endpoints that handle user input validation or resource ownership checks. The fix is a one-line change per route but substantially improves API clarity and ensures that monitoring tools, SDK generators, and HTTP-aware infrastructure all interpret your responses correctly.
When to return 202 Accepted for long-running operations
202 Accepted is the correct status code for operations the server has queued but not yet completed.6 Background jobs, email sends, image processing pipelines, and report generation are all candidates for 202, because the server cannot complete them synchronously within a typical HTTP timeout window. Returning 200 for these operations tells the client the work is done when it is not, which forces the client to treat asynchronous failures as synchronous successes.
A 202 response body should include a polling URL where the client can check the operation status. Some APIs set the Location header on 202 responses pointing to a status resource. Others include a "status_url" field in the response body. Either approach gives the client the address of the status resource without requiring a follow-up GET to discover it.
Polling versus webhooks for 202 completion signals
Your API has two options for notifying clients when a 202 operation completes, and the right choice depends on how long the operation takes and how many clients are waiting on the result. Polling requires the client to make repeated GET requests to the status URL until the response indicates completion, which is straightforward to implement but generates unnecessary request volume when many clients are watching the same operation. Webhooks push a notification to a client-registered URL when the operation finishes, eliminating the overhead of repeated polling requests and delivering the result to the client within seconds of completion rather than on the next poll interval. For operations that complete in under 30 seconds, polling is simpler to implement and the additional request volume is negligible. For operations that may take minutes or hours, webhooks reduce server load from unnecessary polling requests significantly and provide a better client experience.
The 200 response and content negotiation with Accept headers
Content negotiation allows a single endpoint to serve multiple response formats based on the client's Accept request header. When the server responds with 200, the Content-Type response header tells the client which format the body uses. A client requesting Accept: application/json should receive Content-Type: application/json on the 200 response; a client requesting Accept: text/csv should receive Content-Type: text/csv. Mismatching the response Content-Type with the request Accept header causes clients to misparse the body even though the status code is 200.
If the server cannot produce a response in any of the formats listed in the Accept header, it should return 406 Not Acceptable rather than returning 200 with a format the client did not request.6 Many frameworks (Express, ASP.NET, Spring MVC) perform content negotiation automatically when multiple response serializers are registered.
HEAD requests and the 200 response body
HEAD requests use the same semantics as GET but instruct the server to return only the response headers, with no body. A 200 response to a HEAD request must include all the headers that a GET would return, including Content-Length, ETag, and Content-Type.1 This allows clients to check resource existence and metadata without downloading the full body. Your server framework typically handles HEAD automatically by suppressing the body on GET routes that receive HEAD requests, but custom middleware must implement this explicitly if the framework does not.
Verifying that your HEAD responses match GET headers exactly catches middleware bugs that strip headers in one path but not the other. A quick test is to compare the HEAD and GET responses for the same resource using curl: curl -I returns the HEAD headers, and curl -i returns GET with headers and body. The Content-Length, ETag, Cache-Control, and any Vary headers should be identical. If they diverge, your HEAD handler is not mirroring the GET handler correctly, which can break cache validation and conditional requests that depend on consistent header values across both methods.
Try in the tool
Open the HTTP Status Code Reference tool pre-filled to 200 to verify it or try a different one.
Check 200 in the tool →- 1.
R. Fielding, Ed., M. Nottingham, Ed., and J. Reschke, Ed., "HTTP Semantics," RFC 9110, IETF, June 2022. https://www.rfc-editor.org/rfc/rfc9110.txt
- 2.
Mozilla Developer Network, "Cacheable," developer.mozilla.org, accessed June 2026. https://developer.mozilla.org/en-US/docs/Glossary/Cacheable
- 3.
Mozilla Developer Network, "200 OK," developer.mozilla.org, accessed June 2026. https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Status/200
- 4.
R. Fielding, Ed., M. Nottingham, Ed., and J. Reschke, Ed., "HTTP Caching," RFC 9111, IETF, June 2022. https://www.rfc-editor.org/rfc/rfc9111.txt
- 5.
Haim Duplicin, "Duplicating HTTP Status in the Response Body is an Anti-Pattern," haim.dev, 2023. https://haim.dev/posts/2023-04-02-duplicating-http-status-in-response-body-is-an-antipattern/
- 6.
"List of HTTP status codes," Wikipedia, accessed August 2026. https://en.wikipedia.org/wiki/List_of_HTTP_status_codes
Return 201 Created. A 200 OK implies the request succeeded and returned a result, but does not signal that a new resource was created at a new URI. A 201 Created with a Location header pointing to the new resource gives the client everything it needs without a follow-up request. Use 200 only when a POST does not create a new resource.
No. Returning 200 with an error body defeats HTTP-aware infrastructure that makes routing, caching, and retry decisions based on the status code. CDNs may cache the 200 error body and serve it to subsequent clients. API gateways may log the 200 as a success. Always use the appropriate 4xx or 5xx code for error conditions.
Yes, heuristically, if no explicit Cache-Control is set. Browsers may cache a 200 GET response based on the Last-Modified header. To prevent caching, add Cache-Control: no-store to the response. To allow caching with revalidation, use Cache-Control: no-cache, must-revalidate and include an ETag or Last-Modified header.
For a PUT that updates a resource, the response body should contain the updated representation of the resource so the client can confirm what was saved. If you prefer not to return a body, use 204 No Content instead. Returning an empty 200 body is technically valid but forces the client to make a separate GET request to see the current state.
Return 202 Accepted when the server has received the request but has not yet completed processing it. Typical cases: an email send queued for background delivery, an image processing job submitted to a worker queue, or a report generation started asynchronously. CapyToolkit does not store or upload any data you paste into its tools, so you can safely inspect the 202 response body and the status URL it returns when verifying your asynchronous workflow without exposing your infrastructure to a third-party service.
201 Created
201 Created confirms a resource was successfully created. Defined in RFC 9110, Section 15.3.2, it tells the client that the request succeeded and a new resource was created as a result, typically at a URI specified in the Location response header.1 Returning 201 after a resource creation operation is a semantic commitment: it signals to HTTP-aware infrastructure, documentation generators, and API clients that a new entity exists at the given address. Consequently, using 200 instead of 201 for creation requests discards this information and forces clients to infer the creation outcome from the response body rather than the status code. The Location header on a 201 response gives the client a ready-to-use URI for the new resource without a separate discovery request, which reduces round trips and makes API interactions more efficient.
What is 201?
RFC 9110, Section 15.3.2. One or more new resources were effectively created in response to the request. The primary resource created by the request is identified by either a Location header field in the response or the effective request URI if no Location is provided. The response body typically contains a representation of the created resource. 201 responses are not cacheable by default under RFC 9110.2The Location header
A 201 response should include a Location header containing the URI of the newly created resource so that the client can retrieve, update, or delete the resource without constructing the URI from context or guessing the server's URL naming conventions. For a POST to /orders that creates order with id 789, the Location header should be /orders/789, and the client can immediately follow up with a GET to that URI to confirm the resource exists with the expected fields.
Many REST API frameworks allow you to set the Location header in a single line: res.status(201).location('/orders/789').json(order) in Express, or returning a JSONResponse with headers in FastAPI. Some frameworks omit the Location header by default and require explicit configuration, so check your framework's documentation if your 201 responses are missing the header.
A 201 without a Location header is technically valid under RFC 9110, which says the primary resource is identified by the effective request URI if no Location is provided. In practice, clients prefer the explicit header, and including it consistently is a mark of a well-designed API that reduces the chance of client-side URL construction errors.
POST vs PUT for resource creation
Both POST and PUT can create resources, and the correct one depends on who controls the resource identifier. POST is appropriate when the server assigns the new resource's ID: the client sends a body without a specific URI, and the server creates the resource and returns its new URI in the Location header with a 201. PUT is appropriate when the client knows the exact URI of the resource it wants to create or replace: a PUT to /users/johndoe creates or replaces that specific resource, and repeating the same PUT request has no additional effect because the resource is already in the desired state.
Idempotency and creation
PUT with creation semantics is idempotent: repeating the same PUT request produces the same result without creating duplicate side effects, which makes PUT safe to retry when a network failure leaves the client unsure whether the request reached the server. POST is not idempotent: repeating the same POST creates duplicate resources, so retrying a POST without an idempotency key can result in multiple charges, multiple user accounts, or multiple orders for the same logical operation. API design typically uses POST for collection creation and PUT for named-resource upsert, with both returning 201 on successful creation, and choosing between them is one of the most consequential decisions you will make when designing a REST API.
201 vs 200 for creation responses
Choosing 200 over 201 for a resource creation response is a common mistake that discards useful semantic information and makes your API harder for both humans and tooling to understand at a glance. A 201 communicates three things at once: the request succeeded, a new resource was created, and the Location header tells the client where to find it.3 A 200 communicates only that the request succeeded, leaving the client to guess whether a new resource was created or an existing one was returned from the server.
API documentation generators, client SDK generators, and OpenAPI tooling use the 201 status code to mark endpoints that produce new resources, enabling automatic generation of correct client code that knows to follow the Location header to the created resource. Some legacy APIs return 200 for all successful operations to simplify the client handling surface, which works but loses the machine-readable creation signal that downstream tooling relies on to generate accurate code for every endpoint in your API surface.
Returning 201 for an update operation (where 200 is correct) is semantically wrong and confuses clients that use the status code to distinguish creation from update in their business logic, especially in applications that log or audit resource creation events separately from updates and need the status code to drive that distinction without parsing the response body.
How SDK generators interpret 201 responses
Client SDK generators such as the OpenAPI Generator and Swagger Codegen use the 201 status code to decide whether a method returns a newly created resource object or a generic success response, so using 200 instead of 201 for creation endpoints forces the generated client to discard the Location header and the created resource URI. This matters most in larger teams where the SDK is generated from your API specification rather than written by hand, because a single incorrect status code can ripple into every client library that consumes your API and silently break resource-creation workflows across your entire ecosystem.4
201 Created for bulk and batch creation endpoints
Batch creation endpoints present a status code challenge. When a POST to /orders/batch creates 50 orders simultaneously, which status code is correct? A full-success response returns 201 Created with a body listing all created resource URIs. The Location header works in the single-resource case but becomes impractical for batch responses where you cannot list 50 URIs in a single header value; move the URI list to the response body instead.
Partial-success scenarios are more complex. If 48 of 50 items succeed and 2 fail, the operation was neither a complete success nor a complete failure. RFC 4918 defines 207 Multi-Status for exactly this case: the 207 body contains a list of per-item status codes and messages.5 Many REST APIs avoid 207 due to implementation complexity and instead reject the entire batch on any failure, returning 422 with the failing items identified in the error body.6
Idempotent batch creation with upsert semantics
Some batch endpoints use upsert semantics: items that already exist are updated, and items that do not exist are created. An upsert batch that creates 10 new records and updates 5 existing ones returns 200 rather than 201, because not every item in the batch was a new creation. Documenting this behavior in your API specification prevents clients from assuming that a batch endpoint always returns 201. If your batch endpoint exclusively creates new records and rejects duplicates, 201 is correct; if it mixes creation and update, 200 is more accurate.
Testing your batch creation endpoints under concurrent load reveals race conditions that single-request tests miss. When multiple clients send overlapping batch requests for the same resources, the upsert logic must correctly resolve conflicts without creating duplicates or losing updates. A robust test suite simulates concurrent batch submissions and verifies that the final resource state matches the expected outcome regardless of request ordering. This validation is especially important for APIs that use optimistic locking or version-based conflict resolution, because the batch endpoints can silently corrupt data if the conflict resolution logic is not exercised under realistic concurrency.
Try in the tool
Open the HTTP Status Code Reference tool pre-filled to 201 to verify it or try a different one.
Check 201 in the tool →- 1.
R. Fielding, Ed., M. Nottingham, Ed., and J. Reschke, Ed., "HTTP Semantics," RFC 9110, IETF, June 2022. https://www.rfc-editor.org/rfc/rfc9110.txt
- 2.
Mozilla Developer Network, "Cacheable," developer.mozilla.org, accessed June 2026. https://developer.mozilla.org/en-US/docs/Glossary/Cacheable
- 3.
Mozilla Developer Network, "201 Created," developer.mozilla.org, accessed June 2026. https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Status/201
- 4.
"OpenAPI Specification," Wikipedia, accessed August 2026. https://en.wikipedia.org/wiki/OpenAPI_Specification
- 5.
L. Dusseault, Ed., "HTTP Extensions for Web Distributed Authoring and Versioning (WebDAV)," RFC 4918, IETF, June 2007. https://www.rfc-editor.org/rfc/rfc4918.txt
- 6.
IANA, "Hypertext Transfer Protocol (HTTP) Status Code Registry," iana.org, accessed August 2026. https://www.iana.org/assignments/http-status-codes/http-status-codes.xhtml
RFC 9110 recommends it but does not require it. If no Location is provided, the primary resource is identified by the effective request URI. In practice, always include a Location header: it gives clients the exact URI of the new resource and eliminates the need for the client to construct the URI from context or make a follow-up request.
Yes, for most REST APIs. Returning the created resource in the 201 body saves the client a follow-up GET request to see the resource with server-assigned fields like id, createdAt, or default values. If the body would be too large or the client does not need it, return 201 with a Location header and an empty body.
Yes. A PUT to a URI that does not yet exist creates the resource and should return 201 Created. A PUT to a URI that already exists updates the resource and should return 200 OK or 204 No Content. Some APIs return 200 for both cases to simplify client logic, but the distinction helps clients detect whether they created a new resource or modified an existing one.
Typically for simplicity: the API returns 200 for all successful operations so client code only needs one success branch. This works but discards the semantic information that 201 provides. APIs used by other developers benefit from returning 201 because tooling, SDK generators, and OpenAPI validators use the status code to understand which endpoints create new resources.
Not by default. RFC 9110 does not list 201 as cacheable. Caching a creation response would not make sense in most cases: the response represents the newly created resource, and caching it would return stale data on subsequent requests for the same collection. CapyToolkit allows you to inspect the full set of response headers on any endpoint, including the Location header on a 201, so you can verify your server's behavior without writing custom test scripts or exposing your API to a third-party debugging service.
304 Not Modified
304 Not Modified tells the client its cached copy is still valid. Defined in RFC 9110, Section 15.4.5, it is the server's response to a conditional GET or HEAD request: the client includes a cache validator (ETag or Last-Modified) in the request, and the server confirms the resource has not changed since the client's cached version.1 The server returns only the response headers, with no body, dramatically reducing bandwidth for assets that change infrequently. Consequently, 304 is the mechanism behind efficient browser caching for static assets, API responses, and CDN edge caching. Understanding the conditional request flow allows developers to implement cache validation correctly on both the client side and the server side, avoiding the common mistake of either caching too aggressively or re-downloading unchanged resources on every request.
What is 304?
RFC 9110, Section 15.4.5. The condition in the request did not evaluate to true: the client's conditional GET or HEAD request was processed and the representation is not modified since the date or version specified by the client's cache validators. The server sends no message body. The response must include the same headers that would accompany a 200 response, such as Cache-Control, ETag, and Vary, so the client can update its stored response. 304 is not cacheable itself.2ETags and If-None-Match
The ETag response header is a version identifier for a resource: a hash of the content, a version number, or any opaque string that changes when the resource changes. When a client receives a response with an ETag, it stores the value alongside the cached body. On the next request for the same resource, the client includes an If-None-Match: "etag-value" request header. If the server's current ETag matches, it returns 304 with no body and the client uses its cached copy.
Strong vs weak ETags
ETags support both strong and weak comparison, and choosing between them depends on whether your resource includes volatile fields that change without affecting the meaningful content of the response. Strong ETags indicate byte-for-byte equivalence; weak ETags (prefixed with W/) indicate semantic equivalence where some non-significant parts may differ.1 ETags are more reliable than Last-Modified for cache validation because they detect content changes that happen within the same second, which timestamp-based validation misses entirely. Most modern web frameworks generate ETags automatically for static files, but API endpoints that return database-backed resources typically require you to implement ETag generation yourself using either a version counter or a content hash.
Cache validation flow
Understanding the full conditional request flow prevents implementation errors in both clients and servers, and it is the foundation of efficient browser and API caching for any resource that changes infrequently. Getting this flow wrong can lead to stale content being served to users or unnecessary bandwidth consumption when clients re-download unchanged resources on every request.
On first request, the server returns 200 with ETag: "abc123" and the response body. The client stores the body and the ETag. On the second request, the client sends GET /resource with If-None-Match: "abc123". The server compares the current ETag with "abc123". If they match, the resource has not changed and the server returns 304 with no body. The client updates its cached response's headers from the 304 and continues using the cached body without re-downloading the full representation.
Servers that do not implement ETag or Last-Modified generation force clients to always re-download the full body, even when nothing has changed, wasting bandwidth and increasing response latency for every user who visits your site or calls your API. Implementing conditional request support is one of the highest-impact performance improvements you can make for read-heavy workloads.
304 vs 200 in caching strategy
Choosing between 304 validation and aggressive Cache-Control caching depends on how often the resource changes and how stale the client can tolerate. A response with Cache-Control: max-age=3600 tells the browser not to revalidate for one hour. After one hour, it sends a conditional request and receives either 304 (no change) or 200 (new content).
For resources that must always be current, Cache-Control: no-cache, must-revalidate forces a conditional request on every access while still allowing the server to return 304 if nothing changed, saving bandwidth on every request after the initial load.3 The two mechanisms work together: max-age eliminates round trips for stable resources, while ETag-based validation ensures freshness once max-age expires and prevents the client from serving stale content.
Omitting both Cache-Control and ETag forces the browser to re-download the full body on every page load, regardless of whether anything changed, which is one of the most common causes of unnecessary bandwidth usage on websites that serve large JSON payloads or media assets. Adding even a modest max-age or a simple ETag can reduce your bandwidth bill and improve page load times for repeat visitors immediately.
Choosing between ETag and Last-Modified for your cache layer
ETags detect content changes that happen within the same second, while Last-Modified timestamps are only accurate to one second and can miss rapid updates to a resource that is modified multiple times in a short window. Using both together gives you the best of both worlds: the browser first sends a conditional request with the Last-Modified value, and if the server determines the resource has changed since that timestamp, it can then compare the ETag to confirm whether the content itself has actually changed or only the metadata was updated. Prefer ETags for API responses where content changes are frequent and sub-second accuracy matters, and use Last-Modified for static assets where second-level granularity is sufficient.
Generating ETags for database-backed resources
Generating consistent ETags for database-backed resources requires choosing a value that changes whenever the resource content changes and stays stable when it does not. Two approaches work reliably. First, use a version counter: add an integer version column to the database table and increment it on every update. The ETag becomes the version number as a string, which is cheap to compute and unique per resource version. Second, use a content hash: compute an MD5 or SHA-1 hash of the serialized resource fields and use it as the ETag. Content hashes are more complex to compute but detect content-level changes even if the version column is not updated correctly.
Storing the ETag in the database rather than computing it on every response reduces the per-request overhead significantly for large resources. When the GET handler loads the record, it reads the pre-computed ETag and returns it in the response header. The If-None-Match comparison then requires only a string equality check rather than serialising and hashing the full record on every request.
Weak ETags for semantically equivalent representations
Weak ETags (prefixed with W/) indicate that two responses are semantically equivalent but not byte-for-byte identical. A resource that includes a timestamp in its JSON body generates a different hash on every request, which defeats content hashing. Using a weak ETag that excludes volatile fields (timestamps, request IDs) gives clients useful cache validation while avoiding spurious cache misses from metadata-only changes. Generate weak ETags from the stable business fields of your resource, not from the full serialized output.
Validating your ETag implementation with automated tests catches the most common mistakes before they reach production. A test suite should verify that ETags change when resource content changes, remain stable when content is unchanged, and that the server correctly returns 304 for matching If-None-Match headers. Include edge cases: concurrent updates that should generate new ETags, partial updates that modify only volatile fields, and resources that include computed fields not stored in the database. These tests are fast to run and prevent the frustrating scenario where clients cache stale data because the ETag generation logic had a subtle bug that manual testing missed.
Try in the tool
Open the HTTP Status Code Reference tool pre-filled to 304 to verify it or try a different one.
Check 304 in the tool →- 1.
R. Fielding, Ed., M. Nottingham, Ed., and J. Reschke, Ed., "HTTP Semantics," RFC 9110, IETF, June 2022. https://www.rfc-editor.org/rfc/rfc9110.txt
- 2.
Mozilla Developer Network, "304 Not Modified," developer.mozilla.org, accessed June 2026. https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Status/304
- 3.
R. Fielding, Ed., M. Nottingham, Ed., and J. Reschke, Ed., "HTTP Caching," RFC 9111, IETF, June 2022. https://www.rfc-editor.org/rfc/rfc9111.txt
200 OK returns the full resource body. 304 Not Modified returns only headers, telling the client its cached body is still valid. 304 eliminates bandwidth cost: the client uses its stored copy instead of re-downloading. 304 only occurs when the client sends a conditional request with If-None-Match or If-Modified-Since headers.
The server includes an ETag header with the first response. On subsequent requests, the client sends If-None-Match: "etag-value". If the resource ETag matches, the server returns 304 with no body. If the resource changed, the server returns 200 with the new body and a new ETag. The client updates its cache on 200 and reuses the cached body on 304.
Yes, for frequently requested resources that change infrequently. ETags reduce bandwidth and server load by allowing clients to skip re-downloading unchanged responses. For resources that change on every request (like a live feed), ETags add overhead without benefit. Start with static-data endpoints and high-traffic read endpoints where the cache hit rate is likely to be high.
No. A 304 response must not include a message body. The client uses its stored body from the previous 200 response. The 304 includes headers that update the cache metadata: ETag, Cache-Control, Vary, and any other headers that would accompany a 200 response. Sending a body with a 304 is a protocol error.
A 304 means the server confirmed the resource has not changed since your last visit. If the page looks outdated, the previous version (which the server considers current) was already stale before the 304. CapyToolkit does not upload or store any data you paste into its tools, so you can inspect the ETag and Cache-Control headers on your own responses to confirm whether the server is returning the correct validators before investigating the client side.
422 Unprocessable Content
422 Unprocessable Content means the request is syntactically valid but semantically wrong. Defined in RFC 9110, Section 15.5.21 (formerly introduced in WebDAV, now standardised in HTTP), it applies when the server can parse the request body but the content fails business rule or semantic validation.1 Consequently, 422 is distinct from 400 Bad Request: a 400 means the request itself is malformed (missing JSON braces, wrong Content-Type), while a 422 means the request is well-formed but its content is invalid (an end date before a start date, a quantity below the minimum, a reference to a non-existent parent resource). Understanding this distinction produces clearer API error responses that help clients distinguish structural problems from semantic ones, which require different fix strategies.
What is 422?
RFC 9110, Section 15.5.21. The server understands the content type of the request content and the syntax of the request content is correct, but it was unable to process the contained instructions. The response body should contain a representation of the errors that prevented processing. This code applies when the problem is semantic rather than syntactic. 422 responses are not cacheable by default.2422 vs 400: which to return
The 400 vs 422 decision depends on where in the request processing the failure occurs. Return 400 Bad Request when the request body cannot be parsed at all: malformed JSON, incorrect Content-Type, or a missing required field that prevents the request from being structurally valid. Return 422 when the request body parses successfully but the content fails validation rules: a date range where end precedes start, a product quantity below the minimum order, or a reference to a parent resource ID that does not exist.3
The distinction matters for client error handling. A 400 tells the client to fix the request format; a 422 tells the client to fix the request content. An API that returns 400 for all validation failures forces clients to parse the error body to distinguish a missing field (structural) from an invalid value (semantic), which is exactly the information the status code should convey.
Validation error body conventions
A 422 response body should include enough information for the client to identify and fix every validation error in a single response, rather than discovering one error at a time. A common pattern is an errors array containing objects with a field name and a message for each failing validation rule.
Machine-readable error codes
Machine-readable error codes within each error object let clients localise error messages without parsing English strings: "code": "DATE_RANGE_INVALID" is more useful to a client than "message": "end date must be after start date." RFC 7807 (Problem Details for HTTP APIs) provides a standardised schema for error responses that can be extended with a custom errors array for field-level validation failures. Returning a consistent error body structure across all 422 responses reduces the number of error-handling code paths clients need to implement.
REST API design guidance
Consistent use of 422 versus 400 requires team agreement on what constitutes a structural error versus a semantic one, documented in your API style guide so that every engineer on the team returns the same code for the same type of failure. A useful rule of thumb: if the error would be detected by a JSON schema validator without any knowledge of your business rules, it is 400; if it requires business logic or a database lookup to detect, it is 422.
Many teams choose to return 400 for all client validation failures to avoid explaining the distinction in their API documentation. This is pragmatic and reduces the number of status codes your clients need to handle, though it loses the semantic clarity that helps clients distinguish between a malformed request and a business rule violation in their error handling logic.
Knowing your framework's default is important: FastAPI returns 422 for all Pydantic validation failures, while Django REST Framework returns 400 for serializer validation failures. Check which default your framework uses and ensure your API documentation matches the actual behaviour, or override the default to match the convention you have chosen for the rest of your API surface.
Documenting 422 responses in OpenAPI
Document each endpoint that can return a 422 response in your OpenAPI specification so that client generators produce accurate error-handling code and your API documentation lists every field that can trigger a validation failure. Include a description of the error body structure, the machine-readable error codes your API returns, and an example response that shows a typical validation failure with multiple field-level errors. This level of documentation reduces the support burden on your team because clients can handle validation errors correctly without needing to contact your support team for clarification.
422 in API gateways and request validation middleware
API gateways can perform request validation before traffic reaches your application, returning an error for schema violations at the edge. AWS API Gateway's Request Validator feature validates request bodies against a JSON Schema model and rejects requests that do not match. However, AWS API Gateway returns 400 Bad Request for schema validation failures, not 422. This is a deliberate choice: it treats schema violations as structural errors rather than semantic ones. If your API documentation promises 422 for validation errors, clients behind an API Gateway that enforces schema validation will see 400 from the gateway and 422 from your application, which is inconsistent.
Kong and Apigee allow custom response codes for validation failures through plugin configuration. Kong's Request Validator plugin returns 400 by default but supports a verbose_response option that includes field-level error details. Configuring your API gateway's validation response code to match your application's convention prevents clients from writing two separate error-handling branches for the same logical error type.
Combining gateway validation with application-level 422
Layering gateway-level schema validation with application-level semantic validation covers different error classes efficiently. The gateway validates structure: required fields present, correct types, allowed values. Your application validates semantics: date ranges are valid, referenced parent resources exist, business rules are satisfied. A client that sends a structurally valid but semantically invalid request passes gateway validation and receives 422 from your application. A client that sends a structurally invalid request is rejected at the gateway with its configured error code before your application code runs at all.
Testing the gateway-to-application validation boundary prevents the most common misconfiguration: a gateway that passes structurally invalid requests to the application because its schema is too permissive, or an application that duplicates gateway validation and returns 422 for structural errors that the gateway should have caught at 400. An integration test that sends malformed JSON to an endpoint behind your configured gateway and verifies the correct status code (400 from gateway, 422 from application) catches these mismatches before they affect clients in production. This test is especially important after gateway configuration changes or when you update the JSON Schema model used for edge validation.
Try in the tool
Open the HTTP Status Code Reference tool pre-filled to 422 to verify it or try a different one.
Check 422 in the tool →- 1.
R. Fielding, Ed., M. Nottingham, Ed., and J. Reschke, Ed., "HTTP Semantics," RFC 9110, IETF, June 2022. https://www.rfc-editor.org/rfc/rfc9110.txt
- 2.
Mozilla Developer Network, "Cacheable," developer.mozilla.org, accessed June 2026. https://developer.mozilla.org/en-US/docs/Glossary/Cacheable
- 3.
Mozilla Developer Network, "422 Unprocessable Content," developer.mozilla.org, accessed June 2026. https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Status/422
400 Bad Request means the request is structurally broken: malformed JSON, wrong Content-Type, or a missing required field that prevents parsing. 422 Unprocessable Content means the request is well-formed and parseable but its content fails semantic validation, such as an end date before a start date. The client needs different fix strategies for each.
List all of them. Returning only the first error forces the client to fix one issue, resubmit, discover another error, and repeat. A 422 body with an array of all failing fields and messages lets the client fix everything in a single round trip. Include the field path and a machine-readable error code alongside the human-readable message.
RFC 7807 defines a standard error body schema with type (a URI identifying the error type), title, status, detail, and instance fields. It reduces the need to invent a custom error format. Extend it with an errors array for field-level validation failures. Using RFC 7807 also sets Content-Type: application/problem+json, which API clients and tooling recognise.
FastAPI returns 422 Unprocessable Content for Pydantic validation failures by default. This includes type mismatches, missing required fields, and value constraint violations. The 422 body follows a specific structure with a detail array of error objects. You can customise the error handler with exception_handler to change the status code or body format.
No. 422 is not listed as cacheable by default under RFC 9110. Caching a validation error response would block subsequent requests with valid data from reaching the server. Always include Cache-Control: no-store on 422 responses to ensure the client resubmits fresh requests with corrected content. CapyToolkit does not store or upload any data, so you can paste your request body into the built-in network tool to inspect exactly which fields triggered the 422 without sending your payload to a third-party service.