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
:
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:
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:
1type Product @key(fields: "id") {
2 id: ID!
3 name: String!
4 price: Int!
5}
6
7type Query {
8 topProducts: [Product!]!
9}
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:
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.
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
Clients must support receiving deferred query responses as multipart HTTP responses.
This functionality is currently supported in Apollo Client for Web and Kotlin (experimental).
Your supergraph must be one of:
A self-hosted supergraph running the GraphOS Router
Entity-specific requirements
Your subgraphs must each use a subgraph-compatible GraphQL server library.
You must define one or more entities in your subgraph schemas.
Each subgraph must define a reference resolver for each of its entities (or implement the corresponding functionality in your chosen subgraph library).
This is what enables the router to directly access entity fields with followup sub-queries.
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:
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 usegraphql
as a boundary. Clients MUST accomodate any boundary returned by the server inContent-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:
1type Product @key(fields: "id") {
2 id: ID!
3 name: String!
4 price: Int!
5}
6
7type Query {
8 topProducts: [Product!]!
9}
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:
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:
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:
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:
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:
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:
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:
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 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.
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:
1supergraph:
2 defer_support: false