Skip to content
  • http
  • api
  • web-platform
  • backend
  • search

QUERY: A New HTTP Method for Search Requests

Almost every backend has the same endpoint somewhere: a search that started as a clean GET /transactions?ref=... and slowly grew filters until the URL turned into a monster. Add a date range, a set of statuses, a list of account numbers, an amount band, pagination, and eventually something on the wire throws 414 URI Too Long. The usual fix is to switch it to POST /transactions/search, move the filters into a JSON body, and move on.

It works, but POST is the wrong word and most of us know it. A search reads data. It does not create anything. POST is the verb for “I am about to change something,” and using it for a read means saying “please do not change anything, just find me these rows” with the one verb that promises the opposite. Every search API built this way carries the same small lie.

As of June 2026 there is finally a right answer. It is called QUERY, and it landed in RFC 10008 as a Proposed Standard. It is the first genuinely new HTTP method since PATCH in 2010, and once the gap it fills clicks, you will spot that gap all over your own services.

Why GET can’t do it

GET is the correct verb for reading. It is safe, it is idempotent, and the whole HTTP caching machinery is built around it. So the obvious question is why we don’t just use GET for search and be done with it.

The answer is that a real query does not fit in a URL. Take a typical transaction search: a date range, a set of statuses, a handful of account numbers, an amount band, a currency, a channel, pagination. Serialized into a query string it looks like this, and this is a small one:

GET /transactions?from=2026-01-01&to=2026-06-30&status=SETTLED&status=PENDING&accounts=0123456789,0234567890,0345678901&min=1000&max=5000000&currency=NGN&channel=NIP&channel=POS&page=1&size=50

Three things go wrong here, and all three show up in production.

The first is length. HTTP never defined a maximum URL length, so every proxy, load balancer, CDN, and origin server picks its own. In practice you start losing requests somewhere between 2KB and 8KB, and the failure is a truncation or a 414 from a box you may not even own.

The second is encoding. Nested objects, arrays, ranges, none of it has a natural home in a query string. You end up inventing a private dialect of brackets and commas, and then you have to write a parser for it on the server and keep both sides in sync forever.

The third is leakage. URLs get written down everywhere. Access logs, the gateway, the CDN, browser history, your APM traces. The moment a customer’s account number lands in a request path, it is sitting in a dozen log stores, and now it is a compliance conversation. Request bodies are far less likely to be logged by default.

And then there is the wall you hit if you try to be clever and put the filters in a body on a GET. The HTTP spec has said for years that a body on GET has no defined semantics, and real infrastructure acts on that. Some proxies strip it. Some servers ignore it. Building anything you care about on GET-with-a-body is asking for trouble.

Why POST is the wrong tool we all reach for anyway

POST solves the mechanical problem instantly. It takes a body of any size and any Content-Type, so the filters fit and the URL stays short. That is why essentially every search endpoint in the wild, including basically every GraphQL API, is a POST.

The trouble is that POST means something, and what it means is “process this and expect state to change.” When you use it for a read, you throw away three things you actually wanted.

You lose caching. A POST response is uncacheable for all practical purposes. That expensive reconciliation query a hundred clients run every morning with identical filters gets recomputed from scratch every single time, because you told every cache on the path that this is a mutation and none of them will touch it.

You lose safe retries. When a POST times out, the client has no idea whether the request already ran. Retrying might double-charge someone, so it can’t retry blindly. Your HTTP clients, your service mesh, your proxies all know this and refuse to auto-retry POST, which means you end up writing timeout and retry handling by hand for something that was read-only the whole time.

You lose intent. The next engineer who reads POST /transactions/search, or the gateway that inspects it, or the SDK your codegen spits out, all have to assume the worst, because the verb says a write is happening. The fact that it is a safe, repeatable read lives only in your head and maybe a line of documentation nobody reads.

So the situation before RFC 10008 was that you could have a request body or you could have correct semantics, but not both. QUERY is the method that stops forcing the trade.

What QUERY actually is

The one-line version: QUERY is a request that carries a body like POST but is defined as safe and idempotent like GET.

Safe means the server does not treat it as a change to the target resource. The client is not asking for a mutation and must not expect one. Idempotent means running it twice is indistinguishable from running it once, which is the property that makes retries and prefetching legal again. Those two guarantees are precisely what POST cannot offer, and they are what put caching and automatic retry back on the table for a request that finally has room for a real payload.

Side by side, that puts QUERY in a spot neither of the other two can reach:

PropertyGETPOSTQUERY
Safe (no state change)YesNoYes
IdempotentYesNoYes
Carries a request bodyNo defined meaningYesYes
CacheableYesEffectively noYes
Safe to auto-retryYesNoYes

Here is that same transaction search, expressed the way it always should have been:

QUERY /transactions HTTP/1.1
Host: api.example.com
Content-Type: application/json
Accept: application/json

{
  "from": "2026-01-01",
  "to": "2026-06-30",
  "status": ["SETTLED", "PENDING"],
  "accounts": ["0123456789", "0234567890", "0345678901"],
  "amount": { "min": 1000, "max": 5000000 },
  "currency": "NGN",
  "channels": ["NIP", "POS"],
  "page": 1,
  "size": 50
}
HTTP/1.1 200 OK
Content-Type: application/json

{
  "results": [ /* ... */ ],
  "page": 1,
  "total": 1284
}

No invented dialect. No 8KB ceiling. No account numbers in the access log. And the verb itself now tells every hop between the client and the origin that this is a safe, cacheable read, so they are free to treat it like one.

One more thing the spec is deliberate about: QUERY does not define a query language. The body can be JSON, form-encoded pairs, SQL, GraphQL, a JSONPath expression, whatever the resource understands. QUERY is the envelope. What is written on the letter inside is between the client and that specific endpoint.

Where this leaves you

Standard and universal are not the same thing. QUERY is a plain token on the wire, so clients can send it today, but plenty of proxies, gateways, and CDNs still do not recognize the verb and will reject what they cannot classify. That gap closes with time, the way it did for PATCH through the 2010s, and the fact that RFC 10008 came out of Cloudflare and Akamai suggests the edge will move early.

So there are two sensible plays. When you own the whole path, an internal service calling another where you control the client, the server, and every proxy between them, you can adopt QUERY now and take the caching and safe-retry wins immediately. For anything public and browser-facing, let it settle while the intermediaries catch up. In the meantime, shape your POST /search bodies so they map cleanly onto QUERY later, same payload, same Content-Type discipline. Migrating then becomes a routing change instead of a redesign.

The idea underneath all of it is small and worth holding onto. A search is a read. It deserves a verb that says so, and for the first time in over a decade, HTTP has one.

Isaac Olanrewaju is a backend engineer in Lagos, Nigeria, building payment systems, transaction-heavy services, and financial infrastructure for fintechs, banks, and product teams.