HTTP Status Codes Are Not Suggestions. Use Them Correctly.

by Eric Hanson, Backend Developer at Clean Systems Consulting

When everything is 200, nothing is meaningful

If your API returns 200 OK for almost everything, you’ve effectively disabled one of the most important parts of HTTP.

This is more common than teams admit:

  • validation errors returning 200
  • permission issues returning 200
  • partial failures hidden inside a “successful” response

It works—until it doesn’t.

Clients start layering their own logic to interpret responses. Monitoring becomes unreliable. Retries don’t behave as expected. You’ve replaced a well-defined protocol with a custom one that every consumer has to relearn.

HTTP status codes are not decoration. They are part of the contract.

Status codes drive behavior, not just meaning

The biggest misunderstanding is thinking status codes are only descriptive.

They’re not. They actively control behavior across the system:

  • HTTP clients decide whether to retry based on them
  • proxies and CDNs cache based on them
  • load balancers track health using them
  • observability tools aggregate them into error rates

If you misuse them, you’re not just confusing humans—you’re breaking infrastructure.

The minimal set you actually need

You don’t need to memorize the entire RFC. Most APIs can operate correctly with a small, consistent subset.

Success (2xx)

  • 200 OK → standard success with a response body
  • 201 Created → resource successfully created
  • 204 No Content → success with no response body (e.g., delete)

Example:

HTTP/1.1 204 No Content

No JSON payload needed.

Client errors (4xx)

These indicate the client did something wrong.

  • 400 Bad Request → malformed or invalid input
  • 401 Unauthorized → missing/invalid authentication
  • 403 Forbidden → authenticated but not allowed
  • 404 Not Found → resource doesn’t exist
  • 409 Conflict → state conflict (duplicate, version mismatch)
  • 422 Unprocessable Entity → valid structure, invalid semantics

Example:

HTTP/1.1 422 Unprocessable Entity
{
  "error": {
    "code": "INVALID_QUANTITY",
    "message": "Quantity must be greater than 0"
  }
}

Server errors (5xx)

These mean your system failed to handle a valid request.

  • 500 Internal Server Error → generic failure
  • 502 Bad Gateway → upstream dependency failed
  • 503 Service Unavailable → temporary overload or maintenance

These should trigger alerts. If they don’t, your monitoring is broken.

Common misuse patterns (and why they hurt)

Using 400 for everything

HTTP/1.1 400 Bad Request

for:

  • missing auth
  • invalid permissions
  • resource not found

This flattens all client errors into one category. Clients can’t respond intelligently.

A 404 might be handled silently. A 401 might trigger re-authentication. A 403 might show a permission error.

You’ve removed that nuance.

Returning 500 for client mistakes

If a user submits invalid data and you return 500, you’re blaming the system for a client issue.

This:

  • pollutes error metrics
  • triggers unnecessary alerts
  • hides real server problems

Returning 200 with embedded errors

{
  "success": false,
  "error": "Something failed"
}

This breaks:

  • retry mechanisms
  • caching behavior
  • monitoring accuracy

You’ve effectively opted out of HTTP.

Overusing 204

204 No Content is useful, but overusing it removes useful feedback.

Example:

DELETE /orders/123

Returning 204 is fine.

But for operations where clients benefit from confirmation data, returning 200 with a body is often better:

{
  "status": "cancelled",
  "cancelled_at": "2026-04-19T10:00:00Z"
}

Status codes and idempotency

Status codes also interact with idempotency (whether repeating a request produces the same result).

Example:

DELETE /orders/123

First request:

204 No Content

Second request:

404 Not Found

Both are correct. But they signal different states:

  • first → resource existed and was deleted
  • second → resource is already gone

Clients can use this to decide whether to retry or ignore.

If both returned 200, that signal disappears.

Make status codes predictable

The real goal is not perfection. It’s predictability.

For a given type of operation, clients should know what to expect.

Example:

  • create → 201 or 409
  • fetch → 200 or 404
  • update → 200/204 or 404/422
  • delete → 204 or 404

If different endpoints handle similar situations differently, you’ve created inconsistency at the protocol level.

Testing and enforcement

If you don’t enforce status code usage, it will drift.

Add checks at multiple levels:

Contract tests

expect(response.status).toBe(422)

Not just payload validation.

OpenAPI definitions

Explicitly define responses:

responses:
  '200':
    description: Success
  '404':
    description: Resource not found
  '422':
    description: Validation error

Linting

Use tools like Spectral to enforce:

  • allowed status codes per operation
  • consistency across endpoints

This turns conventions into guarantees.

The tradeoff: strictness vs speed

Being precise with status codes takes effort:

  • developers need to think through edge cases
  • more branches in handler logic
  • more detailed tests

But skipping that work creates hidden costs:

  • unreliable retries
  • misleading metrics
  • fragile client integrations

You’re either explicit in your API design, or implicit in your bugs.

What to do differently this week

Pick one endpoint and map out all its outcomes:

  • success
  • validation failure
  • not found
  • permission issue
  • server failure

Assign the correct status code to each.

Then update the implementation and tests to enforce it.

It’s a small change, but it immediately improves how your system behaves under real conditions.

Scale Your Backend - Need an Experienced Backend Developer?

We provide backend engineers who join your team as contractors to help build, improve, and scale your backend systems.

We focus on clean backend design, clear documentation, and systems that remain reliable as products grow. Our goal is to strengthen your team and deliver backend systems that are easy to operate and maintain.

We work from our own development environments and support teams across US, EU, and APAC timezones. Our workflow emphasizes documentation and asynchronous collaboration to keep development efficient and focused.

  • Production Backend Experience. Experience building and maintaining backend systems, APIs, and databases used in production.
  • Scalable Architecture. Design backend systems that stay reliable as your product and traffic grow.
  • Contractor Friendly. Flexible engagement for short projects, long-term support, or extra help during releases.
  • Focus on Backend Reliability. Improve API performance, database stability, and overall backend reliability.
  • Documentation-Driven Development. Development guided by clear documentation so teams stay aligned and work efficiently.
  • Domain-Driven Design. Design backend systems around real business processes and product needs.

Tell us about your project

Our offices

  • Copenhagen
    1 Carlsberg Gate
    1260, København, Denmark
  • Magelang
    12 Jalan Bligo
    56485, Magelang, Indonesia

More articles

The Roles Required to Build Reliable Software

Reliable software isn’t the result of one great developer. It’s what happens when the right roles quietly do their job, every day.

Read more

How I Handle File Uploads in Rails with Active Storage

Active Storage works well out of the box and quietly fails at scale. Here is how to configure it correctly for production, avoid the common traps, and extend it where the defaults fall short.

Read more

Hiring a Senior Backend Developer in Singapore Takes 9 Weeks and S$120K — There Is a Better Way

Nine weeks ago you posted the job. Today you have two maybes, one lowball counteroffer, and a product launch that can't wait any longer.

Read more

Blocks, Procs, and Lambdas — A Practical Guide Without the Confusion

Ruby gives you three ways to package callable code, and most developers cargo-cult the choice. Here's a precise breakdown of the differences that actually affect behavior in production code.

Read more