HTTP Status Codes for Go APIs in a Quick Reference
A quick reference of the HTTP status codes a Go API needs, with net/http constants, when to use each, and how to pick between 400, 401, 403, 404, 409 and 422.

Status codes are the part of an API that every client library, proxy, load balancer and monitoring dashboard understands without reading your documentation. Choosing them well makes the API behave sensibly in tools you never wrote. Choosing them badly (200 with an error in the body, 500 for validation errors) turns your graphs into noise.
Tested with Go 1.25.6, constants and status text printed from net/http
This is a reference for the codes a JSON API needs, with the exact constant names from Go's net/http package. The listing below was printed by a program that reads each constant and its StatusText, so the names are not from memory. The semantics follow RFC 9110. For the design context, see REST API design in Go and the mistakes roundup.
package main
import (
"fmt"
"net/http"
)
type entry struct {
code int
name string // the net/http constant
}
func main() {
codes := []entry{
{http.StatusOK, "StatusOK"}, {http.StatusCreated, "StatusCreated"}, {http.StatusAccepted, "StatusAccepted"},
{http.StatusNoContent, "StatusNoContent"}, {http.StatusPartialContent, "StatusPartialContent"},
{http.StatusMovedPermanently, "StatusMovedPermanently"}, {http.StatusFound, "StatusFound"},
{http.StatusSeeOther, "StatusSeeOther"}, {http.StatusNotModified, "StatusNotModified"},
{http.StatusTemporaryRedirect, "StatusTemporaryRedirect"}, {http.StatusPermanentRedirect, "StatusPermanentRedirect"},
{http.StatusBadRequest, "StatusBadRequest"}, {http.StatusUnauthorized, "StatusUnauthorized"},
{http.StatusForbidden, "StatusForbidden"}, {http.StatusNotFound, "StatusNotFound"},
{http.StatusMethodNotAllowed, "StatusMethodNotAllowed"}, {http.StatusNotAcceptable, "StatusNotAcceptable"},
{http.StatusRequestTimeout, "StatusRequestTimeout"}, {http.StatusConflict, "StatusConflict"},
{http.StatusGone, "StatusGone"}, {http.StatusLengthRequired, "StatusLengthRequired"},
{http.StatusPreconditionFailed, "StatusPreconditionFailed"}, {http.StatusRequestEntityTooLarge, "StatusRequestEntityTooLarge"},
{http.StatusRequestURITooLong, "StatusRequestURITooLong"}, {http.StatusUnsupportedMediaType, "StatusUnsupportedMediaType"},
{http.StatusRequestedRangeNotSatisfiable, "StatusRequestedRangeNotSatisfiable"},
{http.StatusUnprocessableEntity, "StatusUnprocessableEntity"}, {http.StatusLocked, "StatusLocked"},
{http.StatusPreconditionRequired, "StatusPreconditionRequired"}, {http.StatusTooManyRequests, "StatusTooManyRequests"},
{http.StatusRequestHeaderFieldsTooLarge, "StatusRequestHeaderFieldsTooLarge"},
{http.StatusUnavailableForLegalReasons, "StatusUnavailableForLegalReasons"},
{http.StatusInternalServerError, "StatusInternalServerError"}, {http.StatusNotImplemented, "StatusNotImplemented"},
{http.StatusBadGateway, "StatusBadGateway"}, {http.StatusServiceUnavailable, "StatusServiceUnavailable"},
{http.StatusGatewayTimeout, "StatusGatewayTimeout"},
}
for _, c := range codes {
fmt.Printf("%d %-36s %s\n", c.code, "http."+c.name, http.StatusText(c.code))
}
fmt.Println()
fmt.Println("StatusText(499) is empty:", http.StatusText(499) == "", "- nginx uses 499 for a client that closed the connection, but Go does not name it")
fmt.Println("StatusText(422):", http.StatusText(422))
}200 http.StatusOK OK
201 http.StatusCreated Created
202 http.StatusAccepted Accepted
204 http.StatusNoContent No Content
206 http.StatusPartialContent Partial Content
301 http.StatusMovedPermanently Moved Permanently
302 http.StatusFound Found
303 http.StatusSeeOther See Other
304 http.StatusNotModified Not Modified
307 http.StatusTemporaryRedirect Temporary Redirect
308 http.StatusPermanentRedirect Permanent Redirect
400 http.StatusBadRequest Bad Request
401 http.StatusUnauthorized Unauthorized
403 http.StatusForbidden Forbidden
404 http.StatusNotFound Not Found
405 http.StatusMethodNotAllowed Method Not Allowed
406 http.StatusNotAcceptable Not Acceptable
408 http.StatusRequestTimeout Request Timeout
409 http.StatusConflict Conflict
410 http.StatusGone Gone
411 http.StatusLengthRequired Length Required
412 http.StatusPreconditionFailed Precondition Failed
413 http.StatusRequestEntityTooLarge Request Entity Too Large
414 http.StatusRequestURITooLong Request URI Too Long
415 http.StatusUnsupportedMediaType Unsupported Media Type
416 http.StatusRequestedRangeNotSatisfiable Requested Range Not Satisfiable
422 http.StatusUnprocessableEntity Unprocessable Entity
423 http.StatusLocked Locked
428 http.StatusPreconditionRequired Precondition Required
429 http.StatusTooManyRequests Too Many Requests
431 http.StatusRequestHeaderFieldsTooLarge Request Header Fields Too Large
451 http.StatusUnavailableForLegalReasons Unavailable For Legal Reasons
500 http.StatusInternalServerError Internal Server Error
501 http.StatusNotImplemented Not Implemented
502 http.StatusBadGateway Bad Gateway
503 http.StatusServiceUnavailable Service Unavailable
504 http.StatusGatewayTimeout Gateway Timeout
StatusText(499) is empty: true - nginx uses 499 for a client that closed the connection, but Go does not name it
StatusText(422): Unprocessable EntitySuccess: 2xx
| Code | Go constant | Use it for |
|---|---|---|
| 200 | StatusOK | A successful read, or an update that returns the resource |
| 201 | StatusCreated | A resource was created. Set Location to its URL and return the representation |
| 202 | StatusAccepted | Work was accepted and will finish later. Give a status resource (async jobs) |
| 204 | StatusNoContent | Success with nothing to return: a DELETE, or a PUT that echoes nothing. No body allowed |
| 206 | StatusPartialContent | A range request (range requests) |
A POST that creates returns 201, not 200. A DELETE returns 204 (or 200 with a body if you return the deleted resource). A list with no results is a 200 with an empty array, never a 404.
Redirection and caching: 3xx
| Code | Go constant | Use it for |
|---|---|---|
| 301 / 308 | StatusMovedPermanently / StatusPermanentRedirect | The resource has a new permanent URL. 308 keeps the method; 301 may turn a POST into a GET |
| 302 / 307 | StatusFound / StatusTemporaryRedirect | A temporary move. 307 keeps the method |
| 303 | StatusSeeOther | After a POST, send the client to a GET of the result |
| 304 | StatusNotModified | A conditional request matched the ETag or date (ETags). No body |
APIs redirect rarely. When you must move an endpoint, prefer 308 so clients that POST keep working.
Client errors: 4xx
| Code | Go constant | Use it for |
|---|---|---|
| 400 | StatusBadRequest | The request could not be understood: malformed JSON, a wrong type, an unparseable parameter |
| 401 | StatusUnauthorized | No credentials or invalid ones. Send WWW-Authenticate |
| 403 | StatusForbidden | Authenticated, but not allowed to do this |
| 404 | StatusNotFound | No such resource, or one the caller may not know exists |
| 405 | StatusMethodNotAllowed | The method is not supported on this path. Send Allow (the Go 1.22 mux does) |
| 406 | StatusNotAcceptable | You cannot produce what Accept asks for |
| 408 | StatusRequestTimeout | The client was too slow to send the request |
| 409 | StatusConflict | The request conflicts with current state: duplicate unique value, wrong version |
| 410 | StatusGone | Deliberately removed (deprecation) |
| 411, 413, 414 | StatusLengthRequired, StatusRequestEntityTooLarge, StatusRequestURITooLong | A length is missing, the body is too large, the URL is too long |
| 412 / 428 | StatusPreconditionFailed / StatusPreconditionRequired | If-Match did not match / a conditional header is mandatory (optimistic concurrency) |
| 415 | StatusUnsupportedMediaType | The Content-Type of the body is not accepted |
| 416 | StatusRequestedRangeNotSatisfiable | A range outside the resource |
| 422 | StatusUnprocessableEntity | The syntax is fine but the values fail validation |
| 423 | StatusLocked | The resource is locked |
| 429 | StatusTooManyRequests | Rate limited. Send Retry-After (rate limiting) |
| 431 | StatusRequestHeaderFieldsTooLarge | Headers too big |
| 451 | StatusUnavailableForLegalReasons | Blocked for legal reasons |
Server errors: 5xx
| Code | Go constant | Use it for |
|---|---|---|
| 500 | StatusInternalServerError | An unexpected failure: a bug, a failed assumption. Generic body, details in the log |
| 501 | StatusNotImplemented | The method or feature is not supported at all |
| 502 | StatusBadGateway | A proxy or gateway got a bad answer from upstream |
| 503 | StatusServiceUnavailable | Overloaded, in maintenance or shedding load. Send Retry-After |
| 504 | StatusGatewayTimeout | A proxy or gateway timed out waiting for upstream |
Clients and retry libraries treat 5xx as "maybe try again". Do not return 500 for things the client can fix, and do not return 4xx for your own bugs.
The confusions, settled
401 or 403? 401 means "I do not know who you are": no token, expired token, bad signature. 403 means "I know who you are, and you may not". A client should react to 401 by authenticating and to 403 by giving up or asking for access.
403 or 404 for someone else's object? Return 404. A 403 confirms the object exists, which is information leakage (BOLA). Use 403 when the caller can already see the object but not change it.
400 or 422? 400 for requests the server could not parse or that violate the protocol of the API (invalid JSON, wrong type). 422 for requests that parsed and violate a business or validation rule. Some teams use only 400 and put details in the body. Either works if it is consistent and documented.
409 or 422? 409 when the failure depends on the current state of the system, such as a duplicate email or a version mismatch. 422 when the request is invalid on its own.
200 or 204 for a successful PUT? 200 with the updated resource is friendlier to clients; 204 saves bytes. Choose one per API.
201 or 202? 201 when the resource exists now, 202 when it will exist later.
404 or 410? 404 for "not found"; 410 only when you know it was removed on purpose and want to say so.
Writing them in Go
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusCreated) // before any body byte
json.NewEncoder(w).Encode(created)Use the named constants and not bare numbers, so a reader does not need to remember that 422 is "Unprocessable". Call WriteHeader once, before writing the body; a second call is ignored. http.Error is for plain text, so for JSON set the headers yourself.
Go's names and the RFC's
The output shows Request Entity Too Large and Unprocessable Entity. RFC 9110 renamed these to "Content Too Large" and "Unprocessable Content", and Go keeps the older text for compatibility. The numbers are what matter on the wire. Also note that Go has no name for 499; nginx logs 499 when the client closes the connection before the response, so look for it in proxy logs and not in your handlers.
The authoritative list is the IANA HTTP status code registry, the semantics are in RFC 9110 section 15, and the constants are documented in the net/http package.


