Since 1.8.0

Router Support for @defer

Improve performance by delivering fields incrementally


Queries sent to GraphOS Router or Apollo Router Core can use the @defer directive to enable the incremental delivery of response data. By deferring data for some fields, the router can resolve and return data for the query's other fields more quickly, improving responsiveness.

The router's @defer support is compatible with all federation-compatible subgraph libraries. That's because the router takes advantage of your supergraph's existing entities to fetch any deferred field data via followup queries to your subgraphs.

What is @defer?

The @defer directive enables a client query to specify sets of fields that it doesn't need to receive data for immediately. This is helpful whenever some fields in a query take much longer to resolve than others.

Deferred fields are always contained within a GraphQL fragment, and the @defer directive is applied to that fragment (not to the individual fields).

Here's an example query that uses @defer:

GraphQL
1query GetTopProducts {
2  topProducts {
3    id
4    name
5    ... @defer {
6      price
7    }
8  }
9}

To respond incrementally, the router uses a multipart-encoded HTTP response. To use @defer successfully with the router, a client's GraphQL library must also support the directive by handling multipart HTTP responses correctly.

The router's @defer support is compatible with all federation-compatible subgraph libraries, because the deferring logic exists entirely within the router itself.

Which fields can my router defer?

Your router can defer the following fields in your schema:

  • Root fields of the Query type (along with their subfields)

  • Fields of any entity type (along with their subfields)

    • Deferring entity fields is extremely powerful but requires some setup if you aren't using entities already. This is covered in more detail below.

See below for more information on each of these.

Query fields

Your router can defer any field of your schema's Query type, along with any subfields of those fields:

GraphQL
1query GetUsersAndDeferProducts {
2  users {
3    id
4  }
5  ... @defer {
6    products {
7      id
8    }
9  }
10}

With the query above, the router first returns a list of User IDs, then later completes the response with a list of Product IDs.

Entity fields

Your router supports deferring fields of the special object types in your supergraph called entities.

Entities are object types that often define their fields across multiple subgraphs (but they don't have to). You can identify an entity by its use of the @key directive. In the example subgraph schemas below, the Product type is an entity:

GraphQL
Products subgraph
1type Product @key(fields: "id") {
2  id: ID!
3  name: String!
4  price: Int!
5}
6
7type Query {
8  topProducts: [Product!]!
9}
GraphQL
Reviews subgraph
1type Product @key(fields: "id") {
2  id: ID!
3  reviews: [Review!]!
4}
5
6type Review {
7  score: Int!
8}

Entities are query entry points into your subgraphs, and this is what enables your router to defer their fields: the router can send a followup query to a subgraph to fetch any entity fields that it doesn't fetch initially.

Here's an example query that defers entity fields using the subgraphs above:

GraphQL
1query GetProductsAndDeferReviews {
2  topProducts {
3    id
4    name
5    ... @defer {
6      reviews {
7        score
8      }
9    }
10  }
11}

To handle this query, the router first resolves and returns a list of Product objects with their IDs and names. Later, the router completes the response by returning review scores for each of those products.

note
It doesn't matter which subgraph defines a particular entity field! Queries can defer entity fields that are defined across any number of different subgraphs.

Defining entities in your subgraphs

If your subgraphs don't yet include any entities, you need to define some before clients can start deferring their fields in queries.

To learn about creating entities, see this guide.

Requirements for @defer

To use @defer successfully, your supergraph and its clients must meet the requirements listed below. These requirements are divided between general requirements (requirements for using @defer at all) and entity-specific requirements (additional requirements for using @defer with entity fields).

General requirements

Entity-specific requirements

Executing a @defer query

To execute a deferred query on the router, a GraphQL client sends an HTTP request with almost the exact same format that it uses for usual query and mutation requests.

The only difference is that the request must include the following Accept header:

Text
Example header
1Accept: multipart/mixed;deferSpec=20220824, application/json

Note: because the parts are always JSON, it is never possible for \r\n--graphql to appear in the contents of a part. For convenience, servers MAY use graphql as a boundary. Clients MUST accomodate any boundary returned by the server in Content-Type.

How does the router defer fields?

As discussed in this section, the router can defer the following fields in your schema:

  • Root fields of the Query type (along with their subfields)

  • Fields of any entity type (along with their subfields)

The router can defer specifically these fields because they are all entry points into one of your subgraphs. This enables the router to incorporate the deferral directly into its generated query plan.

Query plan example

Consider a supergraph with these subgraphs:

GraphQL
Products subgraph
1type Product @key(fields: "id") {
2  id: ID!
3  name: String!
4  price: Int!
5}
6
7type Query {
8  topProducts: [Product!]!
9}
GraphQL
Reviews subgraph
1type Product @key(fields: "id") {
2  id: ID!
3  reviews: [Review!]!
4}
5
6
7type Review {
8  score: Int!
9}

And consider this query executed against that supergraph:

GraphQL
1query GetTopProductsAndReviews {
2  topProducts { # Resolved by Products subgraph
3    id
4    name
5    reviews {   # Resolved by Reviews subgraph
6      score
7    }
8  }
9}

To resolve all of these fields, the router needs to query both the Products subgraph and the Reviews subgraph. Not only that, but the router specifically needs to query the Products subgraph first, so that it knows which products to fetch reviews for.

When the router receives this query, it generates a sequence of "sub-queries" that it can run on its subgraphs to resolve all requested fields. This sequence is known as a query plan.

Here's a visualization of the query plan for the example query:

This query plan has three steps:

  1. The router queries the Products subgraph to retrieve the id and name of each top product.

  2. The router queries the Reviews subgraph—providing the id of each top product—to retrieve corresponding review scores for those products.

  3. The router combines the data from the two sub-queries into a single response and returns it to the client.

Because the second sub-query depends on data from the first, these two sub-queries must occur serially.

But the result of the first sub-query includes a significant portion of the data that the client requested! To improve responsiveness, the router could theoretically return that portion as soon as it's available.

A defer-compatible client can request exactly this behavior with the @defer directive:

GraphQL
1query GetTopProductsAndDeferReviews {
2  topProducts {
3    id
4    name
5    ... @defer {
6      reviews {
7        score
8      }
9    }
10  }
11}

With this query, the router understands that it can return the result of its first sub-query as soon as its available, instead of waiting for the result of the second sub-query. Later, it returns the result of the second sub-query when it's ready.

Remember, the router can defer the Product.reviews field specifically because it's a field of an entity. Query plans already use entity fields as entry points for their sub-queries, and the router takes advantage of this behavior to power its defer support.

Deferring within a single subgraph

In the previous example, a client defers fields in a query that already requires executing multiple sub-queries. But what if all of a client query's fields belong to a single subgraph?

Consider this client query:

GraphQL
1query GetTopProducts {
2  topProducts { # All fields resolved by Products subgraph
3    id
4    name
5    price
6  }
7}

Because all of these requested fields are defined in a single subgraph, by default the router generates the most basic possible query plan, with a single step:

Now, let's imagine that the Product.price field takes significantly longer to resolve than other Product fields, and a querying client wants to defer it like so:

GraphQL
1query GetTopProducts {
2  topProducts {
3    id
4    name
5    ... @defer {
6      price
7    }
8  }
9}

This is valid! When the router sees this defer request, it generates a different query plan for the query:

Now, the router queries the same subgraph twice, first to fetch non-deferred fields and then to fetch the deferred fields. When the first sub-query returns, the router can immediately return each product's id and name to the client while sending a followup sub-query to fetch price information.

Non-deferrable fields

A query's @defer fragment might include fields that the router can't defer. The router handles this case gracefully with the following logic:

  • The router defers every field in the fragment that it can defer.

  • The router resolves any non-deferrable fields in the fragment before sending its initial response to the client.

  • The router's response to the client still uses multipart encoding to separate @defer fragment fields from other fields, even if some fragment fields couldn't be deferred.

    • This preserves the response structure that the client expects based on its use of @defer.

Example

To illustrate a non-deferrable field, let's look at an example using this subgraph schema:

GraphQL
1type Book @key(fields: "id") {
2  id: ID!
3  title: String!
4  author: Author!
5}
6
7type Author {
8  name: String!
9  books: [Book!]!
10}
11
12type Query {
13  books: [Book!]!
14  authors: [Author!]!
15}

Note in this schema that the Book type is an entity and the Author type is not.

Let's say a client executes the following query:

GraphQL
1query GetAuthors {
2  authors {
3    name
4    ... @defer {
5      books { # Can't be deferred
6        title # CAN be deferred
7      }
8    }
9  }
10}

This query attempts to defer two fields: Author.books and Book.title.

  • Author.books is neither a root Query field nor an entity field (Author is not an entity), so the router can't defer it.

  • Book.title is the field of an entity type, so the router can defer it.

    • If Book.title had any subfields, the router could also defer those fields.

In this case, the router must internally resolve each author's list of associated books before it can send its initial response to the client. Later, it can resolve each book's title and return those Book objects to the client in an incremental part of the response.

Specification status

The @defer directive is currently part of a draft-stage RFC for the GraphQL specification (learn about RFC contribution stages).

The router supports the @defer directive as it's documented in these edits to the RFC, according to the state of those edits on 2022-08-24.

Disabling @defer

Defer support is enabled in the router by default. To disable support, add defer_support: false to your router's YAML config file under the supergraph key:

YAML
router.yaml
1supergraph:
2  defer_support: false
Feedback

Edit on GitHub

Forums