Why this comparison still matters
GET and POST are the two HTTP methods every developer learns first, and they are also the two most misused. The instinct to reach for POST whenever a request feels 'complicated' is widespread, and it produces APIs that ignore HTTP's built-in semantics: caching, idempotency, bookmarkability, and safe retry. The HTTP specification (RFC 9110, HTTP Semantics, obsoleted the older RFC 7231 in 2022) defines these properties precisely, and understanding them changes how you design APIs. This article compares GET and POST on semantics, body, caching, URL length, and REST conventions.
GET: the safe, idempotent read
GET is defined in RFC 9110 as both safe and idempotent. 'Safe' means the method is intended only for information retrieval and should not change server state — a client can issue a GET without worrying about side effects. 'Idempotent' means issuing the request N times has the same effect as issuing it once. GET is also cacheable by default: responses can be stored by browsers, CDNs, and proxies based on Cache-Control headers.
These properties are powerful. A GET URL can be bookmarked, shared in chat, linked from another page, prefetched by the browser, and cached by Cloudflare for hours. A GET response can be served from disk cache without contacting the server at all. Search engines crawl GET URLs. Browser history remembers them. The back button returns to them without re-submitting. All of this is free if you respect the contract.
The contract has teeth. Because GET is safe, the request body is effectively undefined — RFC 9110 says 'a payload within a GET request message has no defined semantics,' and many intermediaries (proxies, CDNs, web servers) strip or reject GET bodies entirely. Putting parameters in the URL query string is the correct way to send data with a GET. URLs have practical length limits: the HTTP spec sets no maximum, but browsers and servers do — Internet Explorer capped URLs at 2,083 characters, and most modern servers handle 8–16 KB before truncating or rejecting. Sensitive data (passwords, tokens, PII) should never go in a GET URL because URLs are logged by servers, proxies, and browser history.
GET requests are vulnerable to CSRF in the sense that a malicious page can trigger them via image tags or fetch — but only if your GET handlers actually change state, which they shouldn't. The 'safe' contract exists precisely to make CSRF on GET a non-issue: if GET doesn't change state, a forged GET does nothing.
POST: the state-changing write
POST is defined as neither safe nor idempotent. It is intended for submitting data to be processed, creating a new resource, or triggering a state change. Two identical POST requests typically create two resources (or charge a credit card twice). POST is not cacheable by default, though RFC 9110 permits caching if explicit Cache-Control headers are present (rare in practice).
POST's strength is unrestricted request bodies. POST bodies can be any content type and any length — JSON, multipart file uploads, GraphQL queries, raw binary. There is no URL length limit because the data is in the body, not the URL. POST is therefore the right method for large payloads, sensitive data (which shouldn't appear in URLs), and complex queries that don't fit in query parameters.
The cost is the loss of HTTP's affordances. POST requests cannot be bookmarked or shared as URLs. They are not cached without explicit configuration. The browser warns on refresh ('Confirm Form Resubmission') to prevent accidental double-submits. Search engines don't crawl POST endpoints. The back button doesn't return to a POST result without re-warning the user. None of these are deal-breakers, but each represents a capability you give up by choosing POST.
Idempotency must be enforced by the application for unsafe operations that shouldn't be repeated. The standard pattern is an idempotency key — a client-generated UUID sent in a header (commonly Idempotency-Key, used by Stripe) that the server stores for 24+ hours. If the same key arrives again, the server returns the cached response instead of re-executing the operation. Without this, a network retry after a timeout can double-charge a customer.
Status codes, idempotency, and the broader method registry
The HTTP method is only half the contract; the response status code is the other half. REST conventions pair methods with status codes: POST that creates returns 201 Created with a Location header pointing to the new resource; GET that succeeds returns 200 OK; DELETE that succeeds returns 200 (with body), 204 No Content (without body), or 202 Accepted (for async processing); PUT returns 200 or 204; PATCH returns 200 with the updated representation. Returning 200 for every response and burying errors in the body is a common API smell that breaks HTTP clients, load balancers, and monitoring tools — and confuses developers who expect standard semantics.
The idempotency key pattern, popularized by Stripe, deserves broader adoption for any non-idempotent operation with real-world consequences. The client generates a UUID and sends it in the Idempotency-Key header; the server stores the request and response for 24+ hours and returns the stored response on retries with the same key. This converts a non-idempotent POST into an effectively idempotent operation, making network retries safe — critical for payments, order placement, and any operation where a duplicate would harm the user. Stripe's documentation on this pattern is worth reading even if you never use Stripe itself.
HTTP defines more methods than GET and POST, all specified in RFC 9110's method registry. HEAD returns headers without the body (useful for checking existence or cache freshness without downloading). OPTIONS describes the methods a resource supports (used in CORS preflight requests). PATCH partially updates a resource (versus PUT's full replacement). DELETE removes a resource. PUT replaces a resource idempotently — same request, same end state. TRACE and CONNECT exist but are rarely used in application code. These methods are not available in HTML forms (only GET and POST), but they are first-class in fetch, axios, and every modern HTTP client. RESTful APIs should use the full set where appropriate rather than collapsing everything into POST out of habit.
Content negotiation rounds out the contract. The Accept header tells the server what content types the client can handle (Accept: application/json, application/xml;q=0.9); the Content-Type header on a request body declares what the client is sending. A well-designed REST API honors both — returning JSON by default, XML if requested, and the right error codes (415 Unsupported Media Type, 406 Not Acceptable) when the negotiation fails.
Side-by-side comparison
When to choose which
Choose GET for any read operation: fetching a resource, listing a collection, searching, filtering, sorting, paginating. If the request only retrieves data and does not modify server state, GET is correct — even if the query is complex. Long filter expressions belong in query parameters; if they exceed URL length limits, that's a signal to switch to POST with a search body (the Elasticsearch pattern), but most searches fit comfortably in URLs. Stripe, GitHub, and Twitter all use GET for search endpoints for good reason — caching and shareable URLs matter.
Choose POST for any operation that creates, modifies, or deletes state: creating a resource, submitting an order, uploading a file, processing a payment, sending a message. Use POST when the request body is large or contains sensitive data that must not appear in URLs. Use POST when the operation is not idempotent by nature (placing an order twice should create two orders) — but always pair it with an idempotency key for payment-like operations where network retries could cause real harm.
The REST conventions codify this: GET for reads, POST for creates, PUT and PATCH for updates, DELETE for deletes. Many teams collapse PUT, PATCH, and DELETE into POST for simplicity (especially in frameworks where HTML forms only support GET and POST), and that's acceptable — but never collapse reads into POST. Read endpoints that require POST are a common API smell: they cannot be cached, cannot be shared, and force every client into a more complex code path.
A few edge cases deserve mention. Long-read queries (full-text search across millions of documents with complex filters) sometimes exceed URL length limits; the convention there is POST to a /search endpoint with the query in the body, accepting the loss of cacheability. GraphQL uses POST for all queries for the same reason — query strings can be large — and accepts the trade-off that queries can't be cached at the HTTP layer. For APIs where caching matters more than query complexity, REST with GET is the better choice.
Sensitive data in URLs is a recurring mistake. API keys, session tokens, passwords, and PII should never appear in query strings because URLs are logged by servers, proxies, CDNs, and browser history. Use POST with a body, or send credentials in headers (Authorization) for token-based auth. If you must pass a token in a URL (e.g., for a pre-signed download link), make it short-lived and scoped.
Conclusion
GET and POST are not interchangeable. GET is the right method for any read-only request — it brings caching, bookmarkability, shareability, and crawlability for free, and the only cost is respecting the URL length limit and keeping sensitive data out of the URL. POST is the right method for any state-changing request — it brings unrestricted bodies and the absence of caching, which is exactly what you want when creating, modifying, or deleting data. The rule is simple: if the request changes server state, use POST; if it only reads, use GET. Following that rule alone puts you ahead of most APIs in the wild.