skip to content

How does a GraphQL subscription operation reach a server that streams results as text/event-stream?

level: juniorimportance: must knowfreq 38%

answer

  1. the stream only flows one way
  2. everything must be said up front
  3. same request keys as a query
  4. Accept asks for text/event-stream
  5. closing the request ends it

basics

~20 s

The operation travels in the HTTP request that opens the stream: a POST body carrying query and variables, or a GET with the same values as URL parameters. An event stream is one-way, so nothing can be sent upstream afterwards.

solid answer

~50 s

A server-sent event stream is a normal HTTP response the server never finishes, so the request half of the exchange is over before the first result arrives. That leaves exactly one place for the subscription document: the opening request. The convention reuses the request shape GraphQL over HTTP already defines — `query`, `variables`, `operationName` in a POST body, or the same values as GET parameters — with `Accept: text/event-stream` asking for a streamed response. The server validates and starts the subscription, then writes `200` plus the streaming content type and emits one event per execution result. There is no subscribe message, no acknowledgement and no stop message, because the client cannot speak on the stream at all: it ends the subscription by aborting the HTTP request. Worth saying out loud in an interview — no GraphQL specification defines this transport; it is a widely implemented convention.

code

graphql · 7 lines
graphql
subscription OnApplication($req: ID!) {
  applicationReceived(requisitionId: $req) {
    id
    candidateName
    submittedAt
  }
}

go deeper

for a junior

Recall the one-way constraint and what follows from it: the subscription document is in the request that opens the stream, and closing that request is how the client unsubscribes. Being able to name the request keys is enough here.

for a middle

Be ready to walk the mechanics end to end — the Accept header that asks for a stream, validation happening before the headers are written, one event per execution result, and the disconnect that the server reads as an unsubscribe.

for a senior

An interviewer expects you to volunteer that no GraphQL specification defines this transport, and to explain the operational consequence of committing the status before the first result: late failures have to be reported inside the stream rather than as a status code.

for a principal

Own the choice itself. Argue when the simplicity of ordinary HTTP requests — shared middleware, shared auth, nothing new to operate — outweighs needing a channel the client can speak on, and say what the design has to become if that need ever appears.

## One request, then a one-way pipe A server-sent event stream is an ordinary HTTP response that the server deliberately never finishes. The client makes one request; the server answers `200` with `Content-Type: text/event-stream` and then keeps writing into the body for as long as it has something to say. What matters for GraphQL is the *shape* of that exchange: the request half is complete before the first byte of the body arrives, and no channel exists to add to it later. Anything the client wants the server to know has to be said in that opening request, or in some entirely separate HTTP request. ## No specification tells you to do this Before the mechanics, the framing an interviewer is usually listening for. The GraphQL specification defines `subscription` as an operation type and describes how a server executes one, and stops there — it says nothing about how results travel. The GraphQL over HTTP working draft covers operations that produce exactly one response and leaves streaming out of scope. So there is no ratified GraphQL-over-SSE transport at all. What exists is a community convention, implemented compatibly by several independent servers and clients. It is a real, widely deployed convention, and it is worth naming it as one rather than attributing it to "the spec". ## The operation rides the opening request The convention reuses the request shape that GraphQL over HTTP already defines for a query or a mutation. A client POSTs the familiar body — `query`, optionally `variables` and `operationName` — and adds an `Accept` header asking for the streaming media type: ```json POST /graphql Accept: text/event-stream Content-Type: application/json {"query":"subscription OnApplication($req: ID!) { applicationReceived(requisitionId: $req) { id candidateName submittedAt } }", "variables":{"req":"REQ-2291"}} ``` A GET form exists too, carrying the same values as URL parameters. It is not merely a stylistic alternative: some stream clients can only issue GET and cannot set request headers at all, which forces the operation into the URL and the credential into a cookie. The server parses and validates the document, resolves the subscription root field to a source of events, and only then writes the response headers and starts emitting one event per execution result. ## What the one-way constraint costs, and what it buys The cost is that the client has no way to say anything more on that stream. There is no subscribe message, no acknowledgement, no stop message, no client-initiated ping. Ending the subscription means *ending the HTTP request*: the client aborts it, the server observes the disconnect, and tears down the source of events so it stops producing. Open the request to start, close it to stop — that is the entire control vocabulary of the simple shape. The buy is that everything else is completely ordinary. Credentials go in a header or a cookie exactly as they do for a query. Existing HTTP authentication middleware, rate limiting, access logging and tracing see a normal request and need no special case. There is no in-band handshake to design, no connection-level payload in which credentials have to be smuggled, and no second protocol to operate and monitor. ## A worked shape Take a job-board graph. A recruiter's dashboard watches 14 open requisitions and wants each new application to appear without a refresh. In the simple shape the page issues 14 requests, each carrying one `applicationReceived` subscription document, and holds 14 responses open. Closing a requisition card aborts exactly one of them; the other 13 are untouched, because they are unrelated HTTP requests that happen to be slow. Nothing has to be correlated, tagged or demultiplexed. That simplicity is the reason the shape exists, and it is why teams reach for it when a page runs one or two subscriptions rather than a dozen. ## Two edges that catch people **The status is committed before the results are.** Once the server has written `200` and the streaming content type, it cannot change its mind. A failure caught *before* the stream opens — a document that will not parse, a variable that will not coerce — can still be an ordinary error response. A failure discovered afterwards has to be reported as an event inside the stream. Servers therefore do as much validation as they can, and establish the source of events, before committing headers. **Nothing here is a socket.** Candidates who have only used a socket subprotocol describe an SSE subscription as if the client connects and then sends a subscribe message. It cannot: there is no "after connecting" in which the client can speak. If a design genuinely needs the client to say something mid-stream — change a filter argument, stop one operation out of several — that has to become a separate HTTP request, and at that point you have left the simple shape behind.

  • With no upstream channel, what actually stops one of these subscriptions?
    The client aborts the HTTP request, and the server treats the disconnect as the unsubscribe signal — it stops consuming the source of events and releases whatever that source held. The server can also end it from its side by finishing the response body when the source completes or fails. There is no in-band stop message to send and none to acknowledge, so a server that never notices disconnects will happily keep producing events for nobody.
  • A subscription document fails validation. Can the client learn that from the HTTP status?
    Only if the server catches it before writing the response headers, which is why servers validate and establish the source of events first. Once `200` and `text/event-stream` are on the wire the status is committed and cannot be revised, so any later failure has to be reported as an event carried inside the stream. That asymmetry is the practical reason to do as much work as possible before the first byte of the body.
  • Why does this transport reuse the same request keys as a query or a mutation?
    Because nothing about the request needs to be different. The document, its variables and the operation name are the same three things a server needs whatever the operation type, and reusing them means the endpoint, the request parsing and every piece of HTTP middleware stay shared. What differs is only the `Accept` header asking for a stream and the fact that the response carries many results rather than one.

The stream is a letter slot that only opens outward. Whatever the server needs to know has to be written on the envelope you push through when you open it, because you will never get another turn.

saying these in an interview costs you the question

  • Says the client sends a subscribe message on the open stream
  • Claims a GraphQL specification defines an SSE subscription transport
  • Assumes a second channel carries client messages upstream
  • Thinks the server can push before any client request
  • Describes it as a duplex connection like a socket
  • Expects a 4xx status after the stream has already opened

context