skip to content

In a batched GraphQL request, what does the single HTTP status code tell you about each operation?

level: middleimportance: should knowfreq 44%

answer

  1. One response, one status line
  2. Ask what the members actually share
  3. The outcome lives inside each element
  4. A failed member carries errors, no data
  5. The body is not always an array

basics

~20 s

Nothing about any individual operation. One HTTP status covers the whole request, and each element of the response array carries its own result envelope, so a client must inspect every element by position to learn which operations actually succeeded.

solid answer

~50 s

A batched request produces one HTTP response, so there is exactly one status line covering every operation inside it. Servers conventionally answer 200 whenever the array itself could be produced, even when some members failed validation or their resolvers errored, so the status describes the transport and the request as a whole rather than any member. Each element of the response array is a complete, independent envelope: a member that failed validation carries `errors` and no `data` key at all, while its neighbours carry data as normal. The consequence for a client is that success cannot be read off the status - it must walk the array by index and hand each element to whichever caller submitted that position. It must also handle the case where the body is **not** an array: a malformed body, an oversized batch or a server that does not accept batching returns a single error envelope instead.

code

json · 9 lines
json
[
  { "data": { "campaign": { "title": "Winter Appeal", "raisedPence": 1830450 } } },
  { "errors": [
      { "message": "Cannot query field 'giftAidEligible' on type 'Donation'.",
        "locations": [ { "line": 1, "column": 34 } ] }
    ] },
  { "data": { "donor": null },
    "errors": [ { "message": "Donor lookup timed out", "path": [ "donor" ] } ] }
]

go deeper

for a junior

Remember the core fact: one HTTP status covers the entire batch and says nothing about individual operations. Success or failure has to be read from each element of the response array.

for a middle

Explain the two scopes of failure. A member that fails validation carries errors and no data key while its neighbours are untouched, whereas a whole-request failure returns no array at all, so a client must check the body shape before indexing into it.

for a senior

Show the operational consequence: status-based availability monitoring goes blind on a batched endpoint, and a transport failure fails every caller in the batch at once. Be ready to say how you would count per-member outcomes instead.

for a principal

Own the contract question. Fusing independent callers into one HTTP exchange makes their failure domains shared, so decide deliberately whether the round-trip saving is worth coupling unrelated features' availability and giving up status-based alerting.

## One status line, many independent outcomes HTTP gives a response exactly one status code. Batching puts many GraphQL operations behind one response, so the status has to describe something they share, and the only thing they share is the HTTP request itself: was the body readable, was it within limits, did authentication succeed, did the server manage to produce a reply at all. It cannot describe outcomes that differ between members, and in a batch they routinely do. What servers actually do, by convention, is answer 200 as soon as they can produce the response array, whatever happened inside it. A batch of three against a charity donations graph might come back like this: ```json [ { "data": { "campaign": { "title": "Winter Appeal", "raisedPence": 1830450 } } }, { "errors": [ { "message": "Cannot query field 'giftAidEligible' on type 'Donation'.", "locations": [ { "line": 1, "column": 34 } ] } ] }, { "data": { "donor": null }, "errors": [ { "message": "Donor lookup timed out", "path": [ "donor" ] } ] } ] ``` One 200. Inside it: a clean success, a document that never executed because it failed validation, and an operation that executed and hit a failure partway through. Anything reading only the status sees a healthy request. ## Two scopes of failure, and only one of them is in the array The distinction worth being precise about is **whole-request** failure versus **per-member** failure. A whole-request failure happens before or outside member execution: the body is not valid JSON, the array exceeds the server's member cap, the content type is unsupported, credentials are missing, the server is overloaded. There is no array to return, so the server answers with a single result envelope or a plain error body - a map, not an array - and it may well use a 4xx or 5xx status, because now the status genuinely does describe the whole thing. A per-member failure is contained. The member's own envelope carries it. A validation failure produces `errors` with no `data` key, because the operation never ran. A failure during execution produces `data` with a hole in it alongside `errors`. Either way, neighbouring members are unaffected and keep their data. This is why a batching client cannot write `results[k]` without first checking that the parsed body is an array of the expected length. The failure mode of assuming an array - reading index 0 of an error map and getting `undefined` - produces a confusing client-side crash that hides a perfectly clear server error message. ## What this does to the newer response media type The GraphQL over HTTP working draft introduces a response media type under which a **request error** - a document that failed to parse or validate - may be reported with a 4xx status rather than the traditional 200-with-errors. That mapping is defined for a single operation, where one status can describe one outcome. A batch breaks the premise: if member 2 failed validation and members 1 and 3 succeeded, there is no status that is truthful for all three. Since the array body is not part of that specification either, the question simply is not answered by it, and servers fall back on 200 with per-member errors. If you are asked how batching interacts with status-code semantics, that is the honest answer: it does not, because batching is outside the document that defines them. ## The client contract that falls out of it A batching client library ends up owning a demultiplexing contract with several obligations, none of which the transport helps with: ```pseudocode send(members): response = httpPost(endpoint, encodeJson(members)) if not response.isSuccessStatus: failEveryPendingCaller(transportError(response.status)) # whole-request scope return body = parseJson(response.body) if not isArray(body) or length(body) != length(members): failEveryPendingCaller(protocolError("batch response shape")) return for k in 0 .. length(members) - 1: deliver(pendingCaller[k], body[k]) # each caller inspects its own data/errors ``` Three obligations show up there. First, a non-success status fails *every* caller in the batch, including the eight whose operations would have been fine on their own - the blast radius of a transport failure is the whole batch. Second, a length mismatch must be treated as a protocol error rather than quietly delivering results to the wrong callers. Third, each caller still has to inspect its own envelope for `errors`, exactly as it would have on an unbatched request. ## Why this bites in practice The most common real-world symptom is a monitoring illusion: an availability dashboard built on HTTP status shows a flat 100% while a specific operation in the batch has been failing validation since a deploy. Nothing at the transport layer is wrong, so nothing at the transport layer reports it. Detecting per-member failures means looking inside the array, either in the server as it builds the response or in the client as it demultiplexes - and it means the alerting story for a batched endpoint has to be designed rather than inherited.

  • When does a batched request come back as something other than an array?
    Whenever the failure is outside member execution: unparseable JSON, a member count over the server's cap, an unsupported content type, missing credentials, or the server refusing the request outright. There is no array to build, so the server returns a single error envelope or plain error body, often with a 4xx or 5xx. A client that assumes an array crashes on it instead of surfacing the message.
  • If one member of a batch fails, what happens to the others?
    Nothing. Each member is an independent operation with its own envelope, so its neighbours execute and return their data as usual. That containment is the useful half of the design - the damaging half is that a transport-level failure has the opposite property and fails every caller in the batch at once.
  • Why does an availability dashboard built on HTTP status mislead you on a batched endpoint?
    Because the status reports that a response was produced, not that the operations inside it succeeded. A member failing validation on every request still rides inside a 200, so error-rate panels stay flat. Per-member outcomes have to be counted explicitly, either where the server builds the array or where the client demultiplexes it.

saying these in an interview costs you the question

  • Expects a status code per operation inside the batch
  • Reads success off the HTTP status and skips the elements
  • Assumes a failed member makes the whole batch fail
  • Assumes the response body is always an array
  • Thinks a 200 means no member returned errors
  • Confuses a request error in one member with a transport failure

context