Skip to main content
  1. Posts/

HTTP Finally Got a QUERY Method

·1944 words·10 mins·
Http Api-Design Caching Web Backend
Dave Amit
Author
Dave Amit
Principal Architect with nearly two decades designing distributed systems and cloud-native platforms. I bring deep systems expertise, hands-on AI-assisted engineering workflows, and a track record of shipping things that actually scale. Currently writing Go and Rust, running on Kubernetes, and exploring what happens when strong architectural judgment meets modern AI tooling.
Table of Contents

So you’ve done this a hundred times: the search request got too big — too many filters, a nested facet tree, forty checkbox values — and it didn’t fit in the URL anymore, so you moved it into a body and sent it as a POST. Except the moment you picked POST, you told every cache, proxy, CDN, and retry layer between client and server the same quiet lie: this might change something, so don’t cache it, and don’t you dare auto-retry it. None of that is true. A search is read-only and perfectly repeatable, and you knew it when you wrote it. But POST was the only body-bearing method with universal support, so you inherited its pessimism as a package deal.

Last month that stopped being the only option. In June 2026 the IETF published RFC 10008, “The HTTP QUERY Method” — Standards Track, Proposed Standard — which started life as the awkwardly-named draft-ietf-httpbis-safe-method-w-body and ground through fourteen working-group revisions before it landed. QUERY is a real, registered HTTP method now. And your stack almost certainly doesn’t speak it yet.

Prefer video? Watch the full breakdown on YouTube.

Three words engineers keep blurring
#

Three properties, because most of us use these words interchangeably and they aren’t.

Safe means read-only as far as the client is concerned: you’re not asking the server to change state, you’re asking it to tell you something. GET is safe. Idempotent means sending the identical request N times lands you where sending it once would — ten retries equal one, which is exactly what lets a retry layer replay after a dropped connection. GET is idempotent; so is DELETE, oddly enough (deleting a thing twice leaves it just as deleted). Cacheable means a response can be stored and replayed for a later request without going back to the origin.

These three ride together in your head because GET has all three, so you never had to separate them. The instant you need a body, they come apart.

The quadrant that was empty
#

Draw the 2×2. On one axis, “carries a request body.” On the other, “safe and idempotent.”

quadrantChart
    title HTTP methods — request body vs. safe & idempotent
    x-axis No request body --> Carries a request body
    y-axis Unsafe / a write --> Safe & idempotent
    quadrant-1 QUERY fills this cell
    quadrant-2 reads, no usable body
    quadrant-3 unsafe, no body
    quadrant-4 writes, with a body
    GET: [0.24, 0.82]
    POST: [0.80, 0.18]
    PUT: [0.70, 0.30]
    DELETE: [0.63, 0.36]
    QUERY: [0.82, 0.86]

GET sits in the safe-and-idempotent corner but has no usable body (more on why in a second). POST carries any body you want, but it’s defined as potentially unsafe and explicitly not guaranteed idempotent, so it sits in the opposite corner. PUT and DELETE are idempotent, but they’re writes. And the cell you actually wanted — safe, idempotent, carrying a body — was empty. For twenty years no standard method meant “this is a read, it’s repeatable, and the parameters don’t fit in a URL.” You had two tools, neither fit, so you grabbed the closer-looking wrong one.

QUERY fills exactly that cell: registered in the HTTP Method Registry with Safe = yes, Idempotent = yes, and it takes a body.

Why “GET with a body” was never real
#

Every few months someone in a thread suggests the obvious hack: GET is the read method, so just put the body on the GET. It feels clever for about ten seconds.

Here’s why it isn’t. RFC 9110, the core HTTP semantics spec, says a client SHOULD NOT generate content in a GET request, and that content on a GET has no generally defined semantics. That’s not “discouraged but works” — the spec assigns your body no meaning, so every implementation on the path is free to do whatever it wants with it. It goes further: a GET with a body might lead some implementations to reject the request and close the connection, because a body on a method that isn’t supposed to have one is a classic request-smuggling vector. So the standards-level answer is that your body may be meaningless and your connection may get dropped. (And in the real world, not the RFC: plenty of proxies quietly strip the body off a GET before it reaches your origin, which is its own special kind of debugging afternoon.) “GET with a body” was never real — it was undefined behavior wearing a read method’s clothes.

POST-for-search is a downgrade, not a shrug
#

Which is why we all reached for POST instead — and that’s not the neutral choice it feels like.

Look at Elasticsearch’s _search, because everyone reading this has shipped something shaped like it. The Query DSL is nested JSON — bool queries, aggregations, filter clauses — that simply can’t be expressed in URL parameters, so the docs steer you to put it in the body. Completely reasonable. But the verb carrying that body is POST, and the URL ceiling is part of why you’re there: browsers and proxies commonly choke on effective URLs somewhere around a couple of kilobytes (there’s no single spec number, and anyone quoting you an exact universal cap is making it up), so a big filter set was never going to survive in the query string anyway.

So you POST, and here’s the bill. POST is potentially unsafe, so every cache on the path treats the response as un-storable by default: that expensive aggregation can’t be reused even for the identical query thirty seconds later. POST isn’t advertised as idempotent, so a well-behaved retry layer won’t replay it after a network blip — for all it knows, replaying might double-charge somebody, even though your search replays perfectly. You didn’t ask for any of that. You inherited it as collateral for picking the wrong verb to carry a body, for a request that’s provably just a read.

QUERY, precisely
#

QUERY is the method that says the true thing. Straight from the RFC abstract: “A QUERY requests that the request target process the enclosed content in a safe and idempotent manner and then respond with the result of that processing. This is similar to POST requests, but QUERY requests can be automatically repeated or restarted without concern for partial state changes.” Similar to POST in that it carries a body; unlike POST in that the spec hands it the two guarantees POST could never make.

Two beats do the real work.

First, the cache key. A QUERY response is cacheable, and RFC 10008 §2.7 states, as a normative MUST, that the cache key for a QUERY request MUST incorporate the request content and its related metadata, not just the request URI. Caching has always keyed on the URL, which is fine when the URL fully identifies the request. But a body-bearing read has two identical URLs pointing at two different questions, distinguished only by their bodies. Folding the body into the cache key is what lets a body-bearing read participate in HTTP caching at all: two QUERY requests to the same URL with different bodies are correctly two different cache entries.

Second, Content-Location, which quietly does your caching homework. A successful QUERY response MAY include a Content-Location header pointing at a URI, and per §2.3 that’s the server making a claim: you can send a plain GET to this URI and retrieve the results of the query you just ran. So the server takes your gnarly forty-filter search and hands back a normal, GET-able, cache-everywhere URL that resolves to the same result set. Complex query in, clean cacheable resource out. (One guardrail: servers MUST fail the request if the Content-Type is missing or inconsistent with the body — QUERY doesn’t let you be vague about what you’re sending.)

What it looks like in your API
#

The search endpoint stops apologizing. Today you write:

POST /products/search HTTP/1.1
Content-Type: application/json

{ "filters": { "category": "climbing", "price": { "lte": 200 } },
  "sort": "rating:desc" }

— and you know, writing it, that the response won’t be cached and won’t be safely retried, because POST.

The QUERY-shaped version is almost identical on the wire, and completely different in what it promises:

QUERY /products/search HTTP/1.1
Content-Type: application/json

{ "filters": { "category": "climbing", "price": { "lte": 200 } },
  "sort": "rating:desc" }
HTTP/1.1 200 OK
Content-Type: application/json
Content-Location: /products/search/results/9f2c1a
Cache-Control: max-age=60

{ "results": [ ... ] }

Same body, same intent — but now the response is honestly cacheable (keyed on that body), the retry layer is free to replay after a dropped connection, and the server has minted a canonical GET-able URL for the same results. The request finally says what it is, and nobody had to lie to the cache.

The catch: ratified isn’t deployed
#

Now the honest part, because everything above is the spec and the spec is not your production path.

QUERY’s value is proportional to how much of your stack understands it, and in mid-2026 most of it doesn’t. Your clients, server framework, reverse proxy, WAF, and CDN each have to grow real support before a QUERY request survives the whole round trip. A lot of them today will treat an unfamiliar method as an opaque token and route it through, or worse, reject it outright as something not on their list — that second case is the one that’ll page you, a WAF that’s never heard of QUERY blocking it as anomalous traffic. Naming which tools and versions do the right thing today would be irresponsible; the support matrix moves week to week. The generalization worth stating: an unknown method might tunnel through an intermediary as a bare token, but the caching and idempotency semantics don’t tunnel with it. Those only show up when a box on the path actually implements QUERY.

Browsers are their own asterisk. Under the Fetch standard’s CORS rules QUERY isn’t safelisted, so a cross-origin QUERY triggers a preflight — but that’s no new tax, since a cross-origin application/json POST already preflights on its content type. The real caveat is that formal Fetch integration is still in progress; the WHATWG Fetch issue (whatwg/fetch #1938) is open at time of writing. So this is not “call fetch(url, { method: 'QUERY' }) today and ship it.” Not yet.

And a new method is a new attack surface. Body-keyed caches open a cache-poisoning question the URL-keyed world never had to think about, an unfamiliar method is a fresh angle for request smuggling, and every WAF that doesn’t parse QUERY is a blind spot until it does. None of that is a reason to avoid it — it’s the reason to roll it out with your eyes open rather than flip it on and hope. A couple of the RFC’s authors work on CDN and edge infrastructure, so it wouldn’t shock me if edge handling of QUERY lands ahead of your web framework, though that’s my inference about where support shows up first, not a roadmap anyone promised.

So the verdict splits. Design toward QUERY now: model your search endpoints as safe, idempotent, body-bearing reads, because that’s what they always were and now the vocabulary exists to say so. Depend on it end-to-end only once you’ve walked a QUERY request through your specific path — client, proxy, WAF, CDN, origin — and watched it come back correct.

The point isn’t a shiny new verb to play with. QUERY is the first HTTP method that lets a body-bearing read tell the truth to the caching and retry machinery that’s been sitting under all of us for decades. Reach for it when your query outgrows the URL and you’re done lying about idempotence to get a body across the wire.

Related

Garbage Collection Under Pressure: Memory Limits in Containers
·2246 words·11 mins
Go Kubernetes Containers Garbage-Collection Memory
GOMAXPROCS Demystified
·1953 words·10 mins
Go Kubernetes Containers Scheduler Performance
GitHub Issues as an AI Agent Task Manager
·2087 words·10 mins
Ai Go Automation Github Devops
Go's Container Problem: A Silent Crisis
·706 words·4 mins
Go Kubernetes Runtime SRE
Your Primary Key Is a Bottleneck (And Scaling Won't Fix It)
·3160 words·15 mins
Postgresql Performance B-Tree Uuid Database-Design