POST vs PUT vs PATCH: one difference that explains the other two
· 10 min read
POST, PUT and PATCH differ by what the request body is allowed to mean. Where RFC 9110 and RFC 5789 draw the line, and why Idempotency-Key has no RFC.

The two canonical threads on this question have accumulated 3.7 million and 1.06 million views since 2011 and 2014. That is a decade of community interest, not search volume. No keyword data was available, and none is implied. The most-linked explanation of POST vs PUT vs PATCH is anchored to RFC 7231, obsoleted in June 2022.
The difference is not a table of verbs. POST and PUT differ in what the request body is allowed to mean: PUT's body replaces the whole resource, POST's body is material the target interprets by its own rules, and PATCH's body is a set of instructions. Nearly every other argument about these three falls out of that one distinction, including the one that costs the most in production: idempotency is a property of the operation, not something the method hands you. POST /charges with a byte-identical body creates two charges. The fix is a header the caller supplies, not a different verb.
The three properties people collapse into two
Safe, per RFC 9110 §9.2.1
Request methods are considered 'safe' if their defined semantics are essentially read-only; i.e., the client does not request, and does not expect, any state change on the origin server as a result of applying a safe method to a target resource.
RFC 9110 is Standards Track, STD: 97, June 2022, and it is the current definition of method semantics. It obsoletes RFC 2818, 7230, 7231, 7232, 7233, 7235, 7538, 7615 and 7694. Safe methods are GET, HEAD, OPTIONS and TRACE, and safety is what a proxy may do unasked: prefetch, crawl, replay.
Idempotent, per RFC 9110 §9.2.2
A request method is considered 'idempotent' if the intended effect on the server of multiple identical requests with that method is the same as the effect for a single such request. Of the request methods defined by this specification, PUT, DELETE, and safe request methods are idempotent.
PUT and DELETE are idempotent; POST and PATCH are not. Two clauses in the same section keep that from licensing blind retries:
A client SHOULD NOT automatically retry a request with a non-idempotent method unless it has some means to know that the request semantics are actually idempotent, regardless of the method, or some means to detect that the original request was never applied.
and on what idempotency does not buy:
the idempotent property only applies to what has been requested by the user; a server is free to log each request separately, retain a revision control history, or implement other non-idempotent side effects for each idempotent request.
The third property is not in the spec at all
The gap between "POST is not idempotent" and "hand-roll deduplication" is bridged by a header with no standard behind it. Idempotency-Key is draft-ietf-httpapi-idempotency-key-header-06, an IETF Internet-Draft reading "Expires: 28 August 2025". Its abstract states the purpose: "The HTTP Idempotency-Key request header field can be used to make non-idempotent HTTP methods such as POST or PATCH fault-tolerant." It expired unpublished, so there is no standard to cite. What remains is a de-facto convention and a lapsed attempt.
Stripe's documentation states the method asymmetry in two sentences:
All
POSTrequests accept idempotency keys. Don't send idempotency keys inGETandDELETErequests because it has no effect. These requests are idempotent by definition.
A key on a GET does nothing because the method is already idempotent. That is exactly why POST needs one. Stripe caches the first request's status and body per key, "regardless of whether it succeeds or fails", and replays that to any retry, including a 500.
POST and PUT, in the spec's own words
The paragraph that settles it
RFC 9110 §9.3.4 is the most precise statement written about this question:
The fundamental difference between the POST and PUT methods is highlighted by the different intent for the enclosed representation. The target resource in a POST request is intended to handle the enclosed representation according to the resource's own semantics, whereas the enclosed representation in a PUT request is defined as replacing the state of the target resource. Hence, the intent of PUT is idempotent and visible to intermediaries, even though the exact effect is only known by the origin server.
POST delegates; PUT replaces. §9.3.4 pins status codes normatively, which PATCH's RFC does not:
If the target resource does not have a current representation and the PUT successfully creates one, then the origin server MUST inform the user agent by sending a 201 (Created) response. If the target resource does have a current representation and that representation is successfully modified in accordance with the state of the enclosed representation, then the origin server MUST send either a 200 (OK) or a 204 (No Content) response to indicate successful completion of the request.
PUT can create. That is a MUST, not an anecdote.
The hedge — and why "PUT wipes your fields" is folklore
The same section then declines to define what everyone assumes it defines:
HTTP does not define exactly how a PUT method affects the state of an origin server beyond what can be expressed by the intent of the user agent request and the semantics of the origin server response.
HTTP states the replacement intent and stops. It does not say a missing member is deleted, nulled or defaulted: that mapping belongs to the server. "My fields got wiped because I left them out" is a common behaviour, not a guarantee: a server that merges on PUT is equally compliant. Inspect the exact body before a PUT rather than trusting the verb.
POST is not "create"
A service that selects a proper URI on behalf of the client, after receiving a state-changing request, SHOULD be implemented using the POST method rather than PUT.
POST is for when the server names the thing, which is the normal case for creation. §9.3.3 covers processing a submission, appending to an existing representation, and recording data, not only creation. MDN's PATCH reference makes the related point: a PUT overwrites an auto-incrementing counter because it replaces the resource, a PATCH may not.
PATCH is not one thing
PATCH is defined by RFC 5789, Standards Track, March 2010, and it is current: the header carries no Obsoletes line, and the IANA HTTP Method Registry still cites only RFC 5789 §2 for it. Note where it is not: PATCH is absent from RFC 9110's §9.1 method table and appears there only in incidental cross-references, so reading RFC 9110 cover to cover will never tell you PATCH exists.
| Media type | Defined in | Body shape | What null does |
|---|---|---|---|
application/json-patch+json |
RFC 6902, Standards Track | Array of operations | n/a — ops are explicit |
application/merge-patch+json |
RFC 7396, obsoletes 7386 | Partial document | Removes the member |
application/x-www-form-urlencoded |
No RFC | Implicit per-field merge | Implementation-defined |
Vendor type, e.g. application/vnd.github+json |
No RFC | Vendor-shaped partial | Vendor-defined |
RFC 5789 §2 explains why there is no default:
Therefore, there is no single default patch document format that implementations are required to support. Servers MUST ensure that a received patch document is appropriate for the type of resource identified by the Request-URI.
No RFC forbids the form-encoded PATCH; the spec declines to pick a winner.
JSON Patch is an array, not a document
RFC 6902 is Standards Track and not obsoleted. Its body is a list of operations against a target, each with an explicit verb (add, remove, replace, move, copy, test) and a JSON Pointer path. Nothing is inferred from absence; the cost is verbosity.
[
{ "op": "replace", "path": "/status", "value": "shipped" },
{ "op": "remove", "path": "/deprecated_flag" }
]
JSON Merge Patch is where null deletes
RFC 7396 takes the opposite approach: a partial document. Three lines of its §2 pseudocode are the ones people get wrong:
define MergePatch(Target, Patch):
if Patch is an Object:
if Target is not an Object:
Target = {}
for each Name/Value pair in Patch:
if Value is null:
if Name exists in Target:
remove the Name/Value pair from Target
else:
Target[Name] = MergePatch(Target[Name], Value)
return Target
else:
return Patch
"field": null does not set a field to null. It removes it. §1 says so in prose:
Null values in the merge patch are given special meaning to indicate the removal of existing values in the target.
Merge patch therefore cannot express "set this field to null". §1 draws the boundary:
This design means that merge patch documents are suitable for describing modifications to JSON documents that primarily use objects for their structure and do not make use of explicit null values. The merge patch format is not appropriate for all JSON syntaxes.
Config files are where this bites: a format that can hold null is exactly the case merge patch cannot serve. Diffing before and after catches it.
What the internet actually sends
Two things the docs rarely say. GitHub's REST API uses PATCH /repos/{owner}/{repo} with a vendor media type, not either RFC's format: a nested partial applied after reading current state with a GET. Vendor-typed partials are most real PATCH traffic. Atomicity is a MUST (RFC 5789 §2):
The server MUST apply the entire set of changes atomically and never provide (e.g., in response to a GET during this operation) a partially modified representation.
A half-applied patch another reader can observe is a spec violation, not a rounding difference.
Why PUT is idempotent and you still lose writes
Idempotency protects against your retries. It does nothing about someone else writing between your read and your write. Conditional requests, RFC 9110 §13, close that gap:
If-Match = "*" / #entity-tag
The round trip uses the validator §9.3.4 gives you for this:
The new validator(s) received in the response can be used for future conditional requests in order to prevent accidental overwrites (Section 13.1).
GET /jobs/123
→ 200 OK
ETag: "e0023aa4f"
PUT /jobs/123
If-Match: "e0023aa4f"
→ 204 No Content
ETag: "a91b7c33d"
If anyone else moved the resource in between, the tag no longer matches and the server returns 412 Precondition Failed instead of clobbering their write. Replay with the fresh tag and it succeeds. That is the answer to "PUT is idempotent, so why do we still lose updates?"
Status codes: which are normative, which are convention
| Situation | Status | Authority |
|---|---|---|
| PUT created a resource | 201 |
RFC 9110 §9.3.4 — MUST |
| PUT updated a resource | 200/204 |
RFC 9110 §9.3.4 — MUST |
| POST created a resource | 201 + Location |
RFC 9110 §9.3.3 — SHOULD |
| PATCH applied | 204 typical |
RFC 5789 §2.1 — "other success codes could be used as well" |
| DELETE enacted, nothing to return | 204 typical |
RFC 9110 §9.3.5 — SHOULD, not MUST |
| DELETE pending, not enacted | 202 |
RFC 9110 §9.3.5 — SHOULD |
| Method known, resource rejects it | 405 + Allow |
RFC 9110 §15.5.6 |
If-Match did not hold |
412 |
RFC 5789 §2.2 |
| Patch format unsupported | 415 + Accept-Patch |
RFC 5789 §2.2 |
| Patch understood, unprocessable | 422 |
RFC 5789 §2.2 |
| Duplicate key, different body | 422 |
expired draft -06, aspirational |
| Duplicate key in flight | 409 |
expired draft -06, aspirational |
Read the Authority column before writing an implementation note. PUT's statuses are MUSTs; PATCH's are not, because §2.1 justifies its 204 example by saying "other success codes could be used as well". DELETE has no mandatory status. §9.3.5 recommends 202, 204 or 200 with SHOULD.
405 is not 501
The 405 (Method Not Allowed) status code indicates that the method received in the request-line is known by the origin server but not supported by the target resource.
501 Not Implemented is for a method the server does not implement at all. So POST /jobs/123 against a read-only resource returns 405 with Allow: GET, HEAD. A browser learns the same from a CORS preflight, as our preflight guide explains.
Two claims that are wrong
PUT is not "update" and POST is not "create"
Neither method connects to what the operation does to your database, and both verbs are chosen before anyone knows whether anything is created or modified. POST /refunds can modify an existing refund's metadata; PUT /refunds/9 can create refund number 9, and must answer 201 when it does. What separates them is who names the URI and what the body means.
Idempotency is not a property of the method
It is a property of the operation; the method is only a hint. PUT /jobs/123 with a fixed body is idempotent. POST /charges with that same body is not: it creates a second charge. "Just use PUT instead of POST" is backwards: switching verbs changes your URI contract, breaks clients, and may be impossible when the server assigns the identifier. The fix is the same Idempotency-Key on the retry.
RFC 5789 §2 concedes the same about PATCH: "PATCH is neither safe nor idempotent as defined by [RFC2616], Section 9.1", then immediately "A PATCH request can be issued in such a way as to be idempotent." RFC 2616 is itself obsolete; RFC 9110 §9.2.2 holds those definitions now.
Method reference
Safe and idempotent per RFC 9110 §9.2.1 and §9.2.2:
| Method | Safe | Idempotent | What the body means | Defined in |
|---|---|---|---|---|
GET |
Yes | Yes | No defined semantics | RFC 9110 §9.3.1 |
HEAD |
Yes | Yes | As GET, no body | RFC 9110 §9.3.2 |
OPTIONS |
Yes | Yes | Communicates support | RFC 9110 §9.3.7 |
POST |
No | No | Material the resource interprets | RFC 9110 §9.3.3 |
PUT |
No | Yes | The replacement state | RFC 9110 §9.3.4 |
PATCH |
No | No by default | Instructions | RFC 5789 §2 |
DELETE |
No | Yes | No defined semantics | RFC 9110 §9.3.5 |
Related Tools & Further Reading
- JSON Data Viewer — read the exact body before a PUT.
- JSON Diff & Compare — confirm what a merge patch did when a member vanished instead of becoming null.
- Why CORS Preflight Failed — the same semantics, from an
OPTIONSrequest.
Written by
Abhay Khant
Abhay Khant is the founder of ToolSura, a privacy-first developer tools platform. Writes about client-side architecture, AI tooling, and the open web.