Docs
Launch GraphOS Studio

Automatic persisted queries

Improve network performance by sending smaller requests


Clients send queries to as HTTP requests that include the string of the to execute. Depending on your graph's schema, the size of a valid query string might be arbitrarily large. As query strings become larger, increased latency and network usage can noticeably degrade client performance.

To improve network performance for large strings, supports Automatic Persisted Queries (APQ). A is a query string that's cached on the server side, along with its unique identifier (always its SHA-256 hash). Clients can send this identifier instead of the corresponding string, thus reducing request sizes dramatically (response sizes are unaffected).

To persist a string, must first receive it from a requesting client. Consequently, each unique query string must be sent to Apollo Server at least once. After any client sends a string to persist, every client that executes that can then benefit from .

Apollo ServerClient appApollo ServerClient appFails to find persisted query stringPersists query string and hashTime passesFinds persisted query stringSends SHA-256 hash of query string to executeResponds with errorSends both query string AND hashExecutes query and returns resultSends SHA-256 hash of query string to executeExecutes query and returns result

are especially effective when clients send queries as GET requests. This enables clients to take advantage of the browser cache and

.

Because identifiers are deterministic hashes, clients can generate them at runtime. No additional build steps are required.

Apollo Client setup

supports without any additional configuration. However, some client-side configuration is required.

To set up in , first import the createPersistedQueryLink function in the same file where you initialize ApolloClient:

index.ts
import { createPersistedQueryLink } from '@apollo/client/link/persisted-queries';

This function creates a link that you can add to your client's

. The link takes care of generating identifiers, using GET requests for hashed queries, and retrying requests with strings when necessary.

Add the link anywhere in the chain before the terminating link. This example shows a basic two-link chain:

import { ApolloClient, InMemoryCache, HttpLink } from '@apollo/client';
import { createPersistedQueryLink } from '@apollo/client/link/persisted-queries';
import { sha256 } from 'crypto-hash';
const linkChain = createPersistedQueryLink({ sha256 }).concat(
new HttpLink({ uri: 'http://localhost:4000/graphql' }),
);
const client = new ApolloClient({
cache: new InMemoryCache(),
link: linkChain,
});

Command-line testing

You can test out directly from the command line. This section also helps illustrate the shape of APQ requests, so you can use it to add APQ support to besides .

This section assumes your server is running locally at http://localhost:4000/graphql.

Every supports the following (which requests the __typename from the Query type):

{
__typename
}

The SHA-256 hash of this string is the following:

ecf4edb46db40b5132295c0291d62fb65d6759a9eedfa4d5d612dd5ec54a6b38
  1. Attempt to execute this on your running server by providing its hash in a curl command, like so:

    curl --get http://localhost:4000/graphql \
    --header 'content-type: application/json' \
    --data-urlencode 'extensions={"persistedQuery":{"version":1,"sha256Hash":"ecf4edb46db40b5132295c0291d62fb65d6759a9eedfa4d5d612dd5ec54a6b38"}}'

    The first time you try this, responds with an error with the code PERSISTED_QUERY_NOT_FOUND. This tells us that hasn't yet received the associated string.

  2. Send a followup request that includes both the string and its hash, like so:

    curl --get http://localhost:4000/graphql \
    --header 'content-type: application/json' \
    --data-urlencode 'query={__typename}' \
    --data-urlencode 'extensions={"persistedQuery":{"version":1,"sha256Hash":"ecf4edb46db40b5132295c0291d62fb65d6759a9eedfa4d5d612dd5ec54a6b38"}}'

    This time, the server persists the string and then responds with the query result as we'd expect.

    The hash you provide must be the exact SHA-256 hash of the string. If it isn't, returns an error.

  3. Finally, attempt the request from step 1 again:

    curl --get http://localhost:4000/graphql \
    --header 'content-type: application/json' \
    --data-urlencode 'extensions={"persistedQuery":{"version":1,"sha256Hash":"ecf4edb46db40b5132295c0291d62fb65d6759a9eedfa4d5d612dd5ec54a6b38"}}'

    This time, the server responds with the result because it successfully located the associated query string in its cache.

Using GET requests with APQ on a CDN

A great application for is running behind a CDN. Many CDNs only cache GET requests, but many queries are too long to fit comfortably in a cacheable GET request. When the APQ link is created with createPersistedQueryLink({useGETForHashedQueries: true}), automatically sends the short hashed queries as GET requests enabling a CDN to serve those requests. For full-length queries and for all , Apollo Client will continue to use POST requests.

CDN Integration

Content Delivery Networks (CDNs) such as

,
Cloudflare
,
Akamai
, and
Fastly
enable content caching close to clients, delivering data with low latency from a nearby server. makes it straightforward to use CDNs with queries to cache full responses while still executing more dynamic queries.

works well with a Content Distribution Network (CDN) to cache full results. By adding the appropriate cache hints, Apollo Server can calculate Cache-Control headers that a CDN can use to determine how long a request should be cached. For subsequent requests, the result will be served directly from the CDN's cache. A CDN paired with 's is especially powerful since can be shortened and sent with an HTTP GET request.

Step 1: Add cache hints to the GraphQL schema

Add cache hints as

to so that knows which and types are cacheable and for how long. For example, this schema indicates that all fields that return an Author should be cached for 60 seconds, and that the posts should itself be cached for 180 seconds:

type Author @cacheControl(maxAge: 60) {
id: Int
firstName: String
lastName: String
posts: [Post] @cacheControl(maxAge: 180)
}

To learn how to define the @cacheControl , specify hints dynamically inside , set a default maxAge for all , and cache fields only for specific users (enabling CDNs to ignore those fields), see

.

For example, to set a default max age other than 0 you can modify the constructor to include cacheControl:

import { ApolloServerPluginCacheControl } from '@apollo/server/plugin/cacheControl';
const server = new ApolloServer({
typeDefs,
resolvers,
// The max age is calculated in seconds
plugins: [ApolloServerPluginCacheControl({ defaultMaxAge: 5 })],
});

After this step, will serve the HTTP Cache-Control header on fully cacheable responses, so that any CDN in front of will know which responses can be cached and for how long. A "fully cacheable" response contains only data with non-zero maxAge; the header will refer to the minimum maxAge value across the whole response, and it will be public unless some of the data is tagged scope: PRIVATE. To observe this header, use any browser's network tab in its dev tools.

Step 2: Enable automatic persisted queries

Often, requests are big POST requests and most CDNs will only cache GET requests. Additionally, GET requests generally work best when the URL has a bounded size. Enabling means that short hashes are sent over the wire instead of full queries, and can be configured to use GET requests for those hashed queries.

To do this, update the client code. Add the link to the constructor before the HTTP link:

import { ApolloClient, InMemoryCache, HttpLink } from '@apollo/client';
import { createPersistedQueryLink } from '@apollo/client/link/persisted-queries';
import { sha256 } from 'crypto-hash';
const link = createPersistedQueryLink({
sha256,
useGETForHashedQueries: true,
}).concat(new HttpLink({ uri: '/graphql' }));
const client = new ApolloClient({
cache: new InMemoryCache(),
link,
});

If you are testing locally, make sure to include the full

including the port number. For example: uri: "http://localhost:4000/graphql".

Make sure to include useGETForHashedQueries: true. Note that the client will still use POSTs for because it's generally best to avoid GETs for non-idempotent requests.

If configured correctly, browser's dev tools should verify that queries are now sent as GET requests, and receive appropriate Cache-Control response headers.

Step 3: Set up a CDN

How exactly this works depends on exactly which CDN you chose. Configure your CDN to send requests to . Some CDNs may need to be specially configured to honor origin Cache-Control headers; for example, here is

. If all is well, cacheable queries should now be saved by the CDN.

Note that requests served directly by a CDN will not show up in the Studio dashboard.

Cache configuration

By default, stores its registry within its local in-memory cache. If you provide a different cache as a top-level option to the ApolloServer constructor, uses that cache instead.

You can also designate a cache specifically for the registry. To do so, provide an instance of your preferred cache class to the ApolloServer constructor as a cache option nested within the persistedQueries options object. The persistedQueries.cache option is a

, which accepts the same configuration options as 's cache object (also a KeyValueCache).

To learn how to configure the in-memory cache, set up an external cache, or write your own cache implementation, see

.

Adjusting cache time-to-live (TTL)

The cache time-to-live (TTL) value determines how long a registered remains in the cache. If a cached 's TTL elapses and the query is purged, it's re-registered the next time it's sent by a client.

's default in-memory store does not specify a TTL for (an APQ remains cached until it is overwritten by the cache's standard eviction policy). For all other

, the default TTL is 300 seconds. You can override or disable this value by setting the ttl attribute of the persistedQueries option, in seconds:

const server = new ApolloServer({
typeDefs,
resolvers,
persistedQueries: {
ttl: 900, // 15 minutes
},
});

To disable TTL entirely, specify null for the value of ttl:

const server = new ApolloServer({
typeDefs,
resolvers,
persistedQueries: {
ttl: null,
},
});

As with the default behavior of the in-memory cache, this leaves in the cache until they are overwritten by the cache's standard eviction policy.

Disabling APQ

You can disable entirely by setting the persistedQueries attribute to false in the ApolloServer constructor options:

const server = new ApolloServer({
typeDefs,
resolvers,
persistedQueries: false,
});
Previous
Cache backends
Next
Auth
Edit on GitHubEditForumsDiscord

© 2024 Apollo Graph Inc.

Privacy Policy

Company