DocumentationDefer and Stream

Enabling Defer and Stream

@defer, @stream, and experimentalExecuteIncrementally() are available in GraphQL.js v17 and newer. Incremental delivery is pending GraphQL specification work.

@defer and @stream allow a GraphQL operation to produce an initial result and later incremental payloads. This is useful when part of a response is slow, large, or naturally delivered over time.

GraphQL.js keeps this feature explicit. You must add the directives to the schema and use the experimental executor.

Add the directives to your schema

If the directives option is passed to GraphQLSchema, the default directive list is replaced. Include specifiedDirectives when adding experimental directives.

import {
  GraphQLDeferDirective,
  GraphQLSchema,
  GraphQLStreamDirective,
  specifiedDirectives,
} from 'graphql';
 
const schema = new GraphQLSchema({
  query,
  directives: [
    ...specifiedDirectives,
    GraphQLDeferDirective,
    GraphQLStreamDirective,
  ],
});

Once a schema includes @defer or @stream, execute operations against that schema with experimentalExecuteIncrementally(). execute() is the single-result executor and will reject schemas that contain the experimental incremental directives.

Execute incrementally

import { experimentalExecuteIncrementally, parse } from 'graphql';
 
const document = parse(`
  query ProductPage {
    product(id: "abc") {
      id
      name
      ...Reviews @defer(label: "reviews")
    }
  }
 
  fragment Reviews on Product {
    reviews {
      body
      rating
    }
  }
`);
 
const result = await experimentalExecuteIncrementally({
  schema,
  document,
});

The result is either a normal ExecutionResult or an incremental result object.

if ('initialResult' in result) {
  sendInitialPayload(result.initialResult);
 
  for await (const subsequentResult of result.subsequentResults) {
    sendIncrementalPayload(subsequentResult);
  }
} else {
  sendSinglePayload(result);
}

GraphQL.js produces the execution results; your server transport is responsible for serializing and delivering them to the client.

Transport framing guidance

experimentalExecuteIncrementally() gives you result objects, not a wire protocol. Pick a transport framing format that your clients already support and test it end to end.

  • HTTP multipart responses for clients that support incremental patches.
  • Server-sent events when your stack already uses event streams.
  • WebSocket message streams for subscription-like transports.

Keep transport concerns separate from execution concerns: validate operation behavior first, then validate framing and client reassembly separately.

@defer

@defer can be applied to fragment spreads and inline fragments. It defers the fragment when if is true or omitted.

query ProductPage($includeReviews: Boolean! = true) {
  product(id: "abc") {
    id
    name
    ...Reviews @defer(if: $includeReviews, label: "reviews")
  }
}

The label argument is optional, but labels must be unique for active @defer and @stream usages in the operation.

@stream

@stream can be applied to list fields. It sends initialCount items in the initial result and streams later items in subsequent payloads.

query Feed {
  feed(first: 100) @stream(initialCount: 10, label: "feed") {
    id
    title
  }
}

initialCount is non-null and defaults to 0.

Resolvers may return normal iterables, promises, or async iterables for list fields. Async iterables are especially useful with @stream because the executor can complete list items as they become available.

Early execution

enableEarlyExecution allows deferred work to begin before all non-deferred work has completed.

const result = await experimentalExecuteIncrementally({
  schema,
  document,
  enableEarlyExecution: true,
});

This can reduce total latency for expensive deferred sections, but it can also increase concurrent work. Measure before enabling it broadly.

Validation limits

GraphQL.js validates the current incremental delivery rules:

  • @defer and @stream are not supported on subscription operations.
  • @stream must be used on list fields.
  • @stream(initialCount:) must be non-null.
  • Active @defer and @stream labels must be unique.
  • Root field usage must follow the current proposal rules.
  • Multiple active @stream instances cannot target the same field instance.

If a fragment is shared between query and subscription operations, use the directive if argument to disable incremental behavior in the subscription.

subscription Events($incremental: Boolean! = false) {
  event {
    ...EventFields @defer(if: $incremental)
  }
}

Cancellation

Incremental execution accepts abortSignal. Aborting stops new payload production and attempts to close async iterators.

const controller = new AbortController();
 
const result = await experimentalExecuteIncrementally({
  schema,
  document,
  abortSignal: controller.signal,
});