Docs
Try Apollo Studio

Apollo Router support for @defer

Improve performance by delivering fields incrementally


📣 Defer support is generally available as of Apollo Router v1.8.0. Learn more about launch stages.

Queries sent to the Apollo Router 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 Apollo 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:

query GetTopProducts {
topProducts {
id
name
... @defer {
price
}
}
}

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

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

What is @defer?

To learn the basics of the @defer directive and how you can use it with your supergraph, first read Deferring query response data with GraphOS.

The remainder of this article covers the Apollo Router's defer implementation in greater depth.

How does the Apollo Router defer fields?

As discussed in this article, the Apollo 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:

Products subgraph
type Product @key(fields: "id") {
id: ID!
name: String!
price: Int!
}
type Query {
topProducts: [Product!]!
}
Reviews subgraph
type Product @key(fields: "id") {
id: ID!
reviews: [Review!]!
}
type Review {
score: Int!
}

And consider this query executed against that supergraph:

query GetTopProductsAndReviews {
topProducts { # Resolved by Products subgraph
id
name
reviews { # Resolved by Reviews subgraph
score
}
}
}

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:

Fetch (products)
Fetch (reviews)
Flatten (topProducts,[],reviews)

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:

query GetTopProductsAndDeferReviews {
topProducts {
id
name
... @defer {
reviews {
score
}
}
}
}

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:

query GetTopProducts {
topProducts { # All fields resolved by Products subgraph
id
name
price
}
}

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:

Fetch (products)

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:

query GetTopProducts {
topProducts {
id
name
... @defer {
price
}
}
}

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

Fetch (products)
Fetch (products)
Flatten (topProducts,[],price)

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 Apollo 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:

type Book @key(fields: "id") {
id: ID!
title: String!
author: Author!
}
type Author {
name: String!
books: [Book!]!
}
type Query {
books: [Book!]!
authors: [Author!]!
}

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:

query GetAuthors {
authors {
name
... @defer {
books { # Can't be deferred
title # CAN be deferred
}
}
}
}

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 Apollo 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 Apollo Router by default. To disable support, add defer_support: false to your router's YAML config file under the supergraph key:

router.yaml
supergraph:
defer_support: false
Edit on GitHub
Previous
Build and run queries
Next
Request format