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 {idname... @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.
Basics of @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:
type Product @key(fields: "id") {id: ID!name: String!price: Int!}type Query {topProducts: [Product!]!}
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 subgraphidnamereviews { # Resolved by Reviews subgraphscore}}}
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:
- The router queries the Products subgraph to retrieve the
id
andname
of each top product. - The router queries the Reviews subgraph—providing the
id
of each top product—to retrieve corresponding review scores for those products. - 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 {idname... @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 subgraphidnameprice}}
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:
query GetTopProducts {topProducts {idname... @defer {price}}}
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 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
.
- This preserves the response structure that the client expects based on its use of
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 deferredtitle # CAN be deferred}}}}
This query attempts to defer two fields: Author.books
and Book.title
.
Author.books
is neither a rootQuery
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.
- If
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:
supergraph:defer_support: false