๐๏ธ 29042026 1145
๐ #api #http
The two formal properties that decide whether an HTTP method is safe to retry, cache, or replay. Misclassifying a method is the root cause of subtle bugs (double charges, lost updates) that survive code review.
The Two Propertiesโ
Safe โ the method is read-only by intent; calling it should produce no observable side effect on the server. Idempotent โ calling the method N>1 times produces the same server-side state as calling it once.
Safety is a stricter promise than idempotency. Every safe method is idempotent; not every idempotent method is safe.
Method Matrixโ
| Method | Safe | Idempotent | Cacheable | Notes |
|---|---|---|---|---|
| GET | โ | โ | โ | Pure read. |
| HEAD | โ | โ | โ | Like GET, no body. |
| OPTIONS | โ | โ | (rare) | Capability discovery, CORS preflight. |
| TRACE | โ | โ | โ | Diagnostic; usually disabled. |
| PUT | โ | โ | โ | Full replace; same payload twice = same result. |
| DELETE | โ | โ | โ | Repeated deletes leave the same state (gone). |
| POST | โ | โ (default) | (only with explicit headers) | The wildcard. Treat as creating something new each call. |
| PATCH | โ | โ (default) | โ | Partial update; depends on the patch representation. |
Idempotency vs Same-Responseโ
State idempotency, not response idempotency. A second DELETE /users/42 returns 404 (the user is already gone), not 204. The server state is the same โ that's what counts. Common confusion: people conflate "same response" with "idempotent" and incorrectly conclude DELETE isn't idempotent.
PUT writes the same content twice โ server state identical โ idempotent. Even if the second PUT returns a different status (e.g. 200 instead of 201), the resource is the same.
Why POST Isn't Idempotentโ
POST /orders typically creates an order. Calling twice creates two orders. Two charges, two shipments. Bad.
To make POST safe to retry, the application layer adds an idempotency key:
POST /charges HTTP/1.1
Idempotency-Key: 7f8e9c10-...
The server stores the result keyed by the idempotency key; replays return the cached result. See idempotency_keys_api_design.
Why PATCH Isn't Idempotent (in general)โ
PATCH /counter body: { "increment": 1 }
Each call shifts state by +1. Not idempotent.
Versus:
PATCH /counter body: { "value": 5 }
Final state is the same regardless of repetition. Idempotent.
PATCH idempotency is a property of the patch payload, not the method. RFC 5789 explicitly notes this. Default to "treat PATCH as non-idempotent unless your patch grammar guarantees it".
Practical Implicationsโ
| Need | What method/protection |
|---|---|
| Read a resource | GET |
| Create with server-assigned ID | POST + idempotency key |
| Create with client-supplied ID | PUT (idempotent natively) |
| Replace a resource | PUT |
| Partial update | PATCH + idempotency key |
| Delete | DELETE |
| Trigger an action ("charge", "send") | POST + idempotency key (always) |
| Bulk read with parameters | GET with query string (URL-cacheable) or POST (if too long; lose caching) |
Caching, Retries, Reverse Proxiesโ
- Idempotent methods can be retried safely by clients, proxies, and SDKs without coordination.
- Safe methods can additionally be cached (subject to
Cache-Control). - Non-idempotent methods must not be auto-retried by infrastructure. Client SDKs (and load balancers) must distinguish.
- HTTP/2 connection close: a non-idempotent in-flight request gets surfaced as a hard error rather than auto-retry, by spec.
Common Pitfallsโ
- POST that "kind of feels like a read" โ search endpoints with complex bodies. Tempting to POST, but loses caching and retry-safety. Prefer GET with long query strings or a dedicated query language.
- DELETE that returns 404 the second time โ that's correct! Don't treat it as an error in the client retry logic.
- PATCH with addition operations โ
+= 1,append, etc. Not idempotent. Use idempotency keys or restructure to PUT. - Idempotent POST without idempotency key โ saying "our POST is idempotent because the body is the same" is not enough; the server must dedupe. Without a key, the server has no stable identifier for "same logical request".
- Retrying POST in load balancers โ proxies that retry on 5xx will double-create unless idempotency is enforced server-side. Most reverse proxies are conservative here for a reason.
- GET with side effects โ analytics endpoints fire on GET pings. Technically violates safety. Browsers prefetch GETs aggressively (link rel=prefetch); a GET that has side effects can fire silently. Use POST/Beacon for analytics events.
Relatedโ
- idempotency_keys_api_design โ how to make POST safe to retry.
- retry_backoff_jitter โ when retries are appropriate.
- http_status_codes_semantics โ what each method's response codes mean.
- idempotent_consumer_pattern โ the message-bus equivalent of the idempotency-key idea.
http_caching_headers(planned) โ Cache-Control, ETag, conditional GET.
Referencesโ
- RFC 9110 ยง9: HTTP Semantics โ Methods
- RFC 5789: HTTP PATCH
- Stripe API: Idempotent Requests