skip to content

Which transports does Spring for GraphQL support for subscriptions, and why can't a plain HTTP POST /graphql handle them the way it handles queries and mutations?

level: middleimportance: should knowfreq 35%

answer

  1. subscription = stream, needs persistent connection
  2. WebSocket graphql-transport-ws subprotocol
  3. RSocket for service-to-service, multiplexed
  4. spring.graphql.websocket.path off by default
  5. @SubscriptionMapping returns Flux/Publisher

basics

~10 s

Subscriptions stream many results over time, so they need a persistent connection: WebSocket (the graphql-transport-ws protocol) or RSocket. A single request/response HTTP POST returns once and closes, so it can't push an ongoing stream.

solid answer

~40 s

Queries and mutations are request/response and fit plain POST /graphql. Subscriptions return a stream of results over time, which needs a long-lived connection. Spring for GraphQL supports subscriptions over WebSocket (using the graphql-transport-ws subprotocol) and over RSocket; newer versions (1.3+) also allow Server-Sent Events over HTTP. WebSocket is the common browser choice: you configure spring.graphql.websocket.path (e.g. /graphql) to open the handler — note it isn't set by default, so subscriptions are effectively off until you configure a transport. On the server you write an @SubscriptionMapping method returning a reactive Publisher (Flux/Mono). RSocket suits service-to-service streaming and multiplexes many streams over one connection. Plain POST can't work because it returns a single response body and closes the exchange, with no channel to keep pushing updates.

code

java · 21 lines
java
// 1) Enable the WebSocket transport (NOT on by default):
//    spring.graphql.websocket.path=/graphql

@Controller
class CommentController {

    private final Sinks.Many<Comment> sink =
        Sinks.many().multicast().onBackpressureBuffer();

    @SubscriptionMapping                         // maps `subscription { commentAdded }`
    public Flux<Comment> commentAdded(@Argument String postId) {
        return sink.asFlux().filter(c -> c.postId().equals(postId));
    }

    @MutationMapping
    public Comment addComment(@Argument String postId, @Argument String text) {
        Comment c = commentService.save(postId, text);
        sink.tryEmitNext(c);                     // pushes to all active subscribers
        return c;
    }
}

go deeper

for a junior

Know subscriptions need WebSocket (or RSocket) and can't run over a one-shot HTTP POST.

for a middle

Name the graphql-transport-ws subprotocol, that the WebSocket path must be configured, and that fetchers return a Flux.

for a senior

Compare WebSocket vs RSocket vs SSE, discuss backpressure and where headers/auth are captured on WebSocket.

for a principal

Reason about transport choice for browser vs internal traffic, proxy/CDN constraints driving SSE, and streaming resource/backpressure limits at scale.

**The three GraphQL operation types.** GraphQL defines `query` (read), `mutation` (write), and `subscription` (a long-lived stream of events, e.g. "notify me whenever a new comment is posted"). Queries and mutations are inherently **request/response**: the client sends one request, the server sends one response, done. Subscriptions are **one request, many responses over time** — a push stream. **Why plain HTTP POST /graphql is insufficient for subscriptions.** A classic HTTP request/response exchange delivers exactly one response body and then the exchange completes. There is no mechanism to keep pushing further payloads on that same completed exchange. So subscriptions require a transport that keeps a **persistent, bidirectional (or server-push) channel** open. **Transports Spring for GraphQL offers:** 1. **WebSocket** — the workhorse for browsers. Spring implements the **`graphql-transport-ws`** subprotocol (from the graphql-ws library): the client and server exchange framed messages (`connection_init`, `subscribe`, `next`, `complete`, `error`) over one socket. You must **opt in by setting a path**: ```properties spring.graphql.websocket.path=/graphql ``` Until you set this, no WebSocket handler is registered and subscriptions have no transport. (You also need a WebSocket-capable server; in WebFlux this is native, in Web MVC the WebSocket support is used.) 2. **RSocket** — a reactive binary protocol with first-class streaming and multiplexing. Great for **service-to-service** subscriptions. Configure `spring.graphql.rsocket.mapping` (e.g. `graphql`) so a GraphQL route is exposed on your RSocket server; clients use `RSocketGraphQlClient`. 3. **Server-Sent Events (SSE) over HTTP** — added in **Spring for GraphQL 1.3** via the `graphql-sse` protocol. This lets subscriptions ride HTTP without WebSocket, using a long-lived streaming HTTP response. Useful where WebSocket is awkward (some proxies/CDNs). **The server programming model.** A subscription data fetcher returns a **reactive `Publisher`** — typically a Project Reactor `Flux`: ```java @SubscriptionMapping public Flux<Comment> commentAdded(@Argument String postId) { return commentPublisher.forPost(postId); // emits a Comment per event } ``` Spring subscribes to that `Flux`, and each emitted item is serialized and pushed as a `next` message to the client; completion sends `complete`, an error sends `error`. **Gotchas / when-to-use.** - **Subscriptions are off until a transport is configured.** Forgetting `spring.graphql.websocket.path` is the classic "my subscription 404s / doesn't connect" bug. - The WebSocket endpoint path is independent of the HTTP `/graphql` path (you can point both at `/graphql`; the protocol upgrade distinguishes them). - Cross-cutting concerns (auth, headers) on WebSocket use **`WebGraphQlInterceptor`** just like HTTP, but note the WebSocket handshake only carries headers on the initial `connection_init`; per-message HTTP headers don't exist afterward. - Prefer WebSocket for browser clients, RSocket for internal reactive microservices, SSE when you need HTTP-only server push. - Backpressure: because the fetcher returns a reactive `Publisher`, Reactor's backpressure applies end-to-end over WebSocket/RSocket.

  • A developer's subscription query connects but never receives data, and everything else works. What's the first thing you check?
    Whether a subscription transport is configured — most often spring.graphql.websocket.path is unset, so no WebSocket handler exists. Also confirm the client uses the graphql-transport-ws subprotocol and the @SubscriptionMapping actually emits on its Flux.
  • Why return a Flux from a subscription method instead of a List?
    A List is finite/materialized; a subscription is an open-ended, lazy, asynchronous stream. Flux is a reactive Publisher that emits items over time with backpressure, which is exactly what the WebSocket/RSocket transport pushes to the client as `next` messages.

saying these in an interview costs you the question

  • Claiming subscriptions work over a normal POST /graphql request
  • Thinking WebSocket subscriptions are enabled by default
  • Confusing the subprotocol name (graphql-transport-ws) or inventing one
  • Saying a subscription method should return a List/collection instead of a Publisher

context