Caching graphql query results with Strapi

Is there any possible way to cache the graphql queries and results in Strapi ?

As there is already a known issue with graphql being very slow with Strapi.

Hi jainsuneet,

Yes there is! The Strapi GraphQL plugin is built using Apollo Server. If you configure the GraphQL plugin settings you can enable caching and automatic persisted queries for Apollo Server. With HTTP based caching and APQ enabled, your client applications (iOS, Android, Web) talking to your GraphQL API can make use of those Apollo features quite easily (the assumption is that you use a Apollo Client library to avoid having to implement the logic around cache control headers, hashing queries, and retrying requests when the registry does not recognize your APQ hash [Apollo has documentation on all of this]).

Do you have any example of such implementation ? It will be really helpful to see a sample application

Hi jainsuneet,

Unfortunately, our implementations are private. I’ve created a small boilerplate for you that should help you get started in your extensions/graphql/config/settings.js file

3 Likes

@jwerle Thank you so much, it worked like a charm. Just one query, is there any way to clear the cache for a particular content type if the content type data is updated ?

Hi @jainsuneet,

Controlling the cache control hint for a particular content type that you’ve created is pretty straightforward. You can define a customer resolver and set the cache hint dynamically in your resolver. I hope this information helps

1 Like

@jwerle I would like to clear the cache of a particular collection type if the data is updated from strapi admin or by api calls. So I need to somehow clear that particular cache in the collection lifecycle hook (afterUpdate)

Hi @jainsuneet,

Achieving that easily is difficult, but there are work arounds available. I suggest you send in a “nonce” value or something random in your request query variables coming from your client when you want to “bust” the cache. You may want to do this after a mutation. The apollo cache plugin will use values in the query variables for building the cache key.

Hi!
First of all, thanks for the perfect example, this is super helpful and I can’t understand how it’s not a simple config option on the strapi graphql plugin.

However, in case anyone else arrives here and finds themselves in my situation, where you implemented the cache plugin exactly by the book but it just seems to be ignored:
I got this to work only in version 0.9.0 of apollo-server-plugin-response-cache, as the async rewrite of the hooks in newer versions apparently made apollo ignore them (perhaps this is due to strapi using apollo server 2?)

Hope this might save others the time I spent hunting for a solution >_>

1 Like

Hi!
I’m kinda struggling trying to implement cache into my GraphQL queries at server-side.
Right now when we go past 50-100 visitors at the same time, the node server starts struggling handling the GraphQL queries making the website really really slow.
I think what we need is some kind of server-cache for our GraphQL queries, and like a max age of 6 days to refresh or so.
The thing is, we have quite a rare custom implementation (we use strapi for the backend but the website is fully php, we make GraphQL queries through Guzzle plugin, so we’re not using an Apollo client).
I’ve seen Guzzle middleware caches but isn’t this only client based? won’t this still require to query the db for the first time queries of a client? Maybe I got this wrong, but I think what we need is our queries pre-cached on server side…

Ideal scenario: user queries graphql > if it doesn’t find it cached, executes query and server stores cached result // if it finds it cached > result is provided (not querying the bbdd)

Is it possible to configure the strapi graphql server to do this, and not using Apollo clients?
Thanks!

I pass 6 day full time to figure out why it’s not working on strapi v4.

this is the plugins.js file I build to fix the problem (i’am not a js dev sorry for the hacky code)

const MAX_AGE = 3600;
async function sessionId(requestContext) {
    return null;
}
async function shouldReadFromCache(requestContext) {
    return true;
}
async function shouldWriteToCache(requestContext) {
    return true;
}
async function extraCacheKeyData(requestContext) {
}
async function injectCacheControl(requestContext) {
    return {
        requestDidStart(requestContext) {
            return {
                willSendResponse(requestContext) {
                    requestContext.overallCachePolicy.policyIfCacheable.maxAge = "1000";
                    requestContext.overallCachePolicy.policyIfCacheable.scope = "PUBLIC";
                }
            };
        }
    };
}
function setRedisCacheIfEnvSet() {
    if (process.env.REDIS_CACHE_HOST) {
        const { RedisCache } = require('apollo-server-cache-redis');
        const Redis = require('ioredis');
        return new RedisCache({
            client: new Redis({
                host: process.env.REDIS_CACHE_HOST
            }),
        });
    }
    return false;
}
module.exports = ({ env }) => ({
    graphql: {
        config: {
            endpoint: '/graphql',
            shadowCRUD: true,
            depthLimit: 7,
            amountLimit: 2000,
            apolloServer: {
                cache: setRedisCacheIfEnvSet(),
                tracing: false,
                plugins: [
                    apolloServerPluginResponseCache({
                        shouldReadFromCache,
                        shouldWriteToCache,
                        extraCacheKeyData,
                        sessionId,
                    }),
                    injectCacheControl()
                ]
            },
        },
    },
});

1 Like

not working, policyIfCacheable failed when reading for some reason ¯_(ツ)_/¯

{"errors":[{"message":"overallCachePolicy.policyIfCacheable is not a function","extensions":{"code":"INTERNAL_SERVER_ERROR","exception":{"stacktrace":["TypeError: overallCachePolicy.policyIfCacheable is not a function","    at Object.willSendResponse (/home/jchardon/projects/strapy_cloudrun/strapi/node_modules/apollo-server-koa/node_modules/apollo-server-core/dist/plugin/cacheControl/index.js:118:66)","    at /home/jchardon/projects/strapy_cloudrun/strapi/node_modules/apollo-server-koa/node_modules/apollo-server-core/dist/utils/dispatcher.js:12:31","    at Array.map (<anonymous>)","    at Dispatcher.callTargets (/home/jchardon/projects/strapy_cloudrun/strapi/node_modules/apollo-server-koa/node_modules/apollo-server-core/dist/utils/dispatcher.js:9:29)","    at Dispatcher.invokeHook (/home/jchardon/projects/strapy_cloudrun/strapi/node_modules/apollo-server-koa/node_modules/apollo-server-core/dist/utils/dispatcher.js:20:33)","    at sendResponse (/home/jchardon/projects/strapy_cloudrun/strapi/node_modules/apollo-server-koa/node_modules/apollo-server-core/dist/requestPipeline.js:228:26)","    at processGraphQLRequest (/home/jchardon/projects/strapy_cloudrun/strapi/node_modules/apollo-server-koa/node_modules/apollo-server-core/dist/requestPipeline.js:177:12)","    at processTicksAndRejections (internal/process/task_queues.js:95:5)","    at async processHTTPRequest (/home/jchardon/projects/strapy_cloudrun/strapi/node_modules/apollo-server-koa/node_modules/apollo-server-core/dist/runHttpQuery.js:220:30)","    at async /home/jchardon/projects/strapy_cloudrun/strapi/node_modules/apollo-server-koa/dist/ApolloServer.js:82:59","    at async bodyParser (/home/jchardon/projects/strapy_cloudrun/strapi/node_modules/koa-bodyparser/index.js:95:5)","    at async returnBodyMiddleware (/home/jchardon/projects/strapy_cloudrun/strapi/node_modules/@strapi/strapi/lib/services/server/compose-endpoint.js:52:18)","    at async policiesMiddleware (/home/jchardon/projects/strapy_cloudrun/strapi/node_modules/@strapi/strapi/lib/services/server/policy.js:24:5)","    at async /home/jchardon/projects/strapy_cloudrun/strapi/node_modules/@strapi/strapi/lib/middlewares/logger.js:22:5","    at async /home/jchardon/projects/strapy_cloudrun/strapi/node_modules/@strapi/strapi/lib/middlewares/powered-by.js:16:5","    at async cors (/home/jchardon/projects/strapy_cloudrun/strapi/node_modules/@koa/cors/index.js:95:16)"]}}}]}

first request write in redis, second request crashing event when shouldReadFromCache and shouldWriteToCache is disable .

Hi

I’m having trouble with Apollo GraphQL caching. I’m using the same multiple collection type query for quering all entities and single entity. This is because I need to query single entity based on a slug for URL friendly endpoints. Since the “__typeName” coming back is “Project” on both of the cases but query bodies are different, Apollo warns that cache data might be lost since attributes content is different. I don’t want to query all the content of each project for listing the projects. Any idea on how to solve this ?

Thanks

I’m implementing this approach to cache graphql query

Strapi.io — Boost graphql performance with Redis cache | by Sofyan Hadi Ahmad | Medium But, it’s for v3. Plan to upgrade to v4

Is there any out of the box cache solution for strapi v4?

Have been spending 2 weeks to solve this problem. Hope This help.
My use case is simple, just want to cache every post query with certain period.

By using the code sample from Helldrum, I have encounter three main problems.

  1. Strapi v4 with Graqhql plugin v4 is actually using Apollo v3, when I am trying to get the plugin, the pages prompt me to get the latest version for v4, which is not working for sure.
  2. For some versioning issue, RedisCache do not accept ioredis instance, but a host string instead.
  3. The default max age setting in config in plugin.js doesn’t work, also not working by using resolverConfig & injectCacheControl function. The plugin will always ignore and do not cache.

The solution is to follow apollo v3 doc, using the “ApolloServerPluginCacheControl” to set the defaultMaxAge.

1 Like

Hi @Roy thank you for your helpful comments!

We are also trying to implement this, but we did not have any success. Is your solution working? Would you please share your code and versions of the packages that you are using? Thank you!

Sorry for the late reply, here it is. In the example, I use InMemoryLRUCache, you can switch to use redis.

I currently run into another problem. The plugin apollo-server-plugin-response-cache does not invalidate cache after mutation, and I figure out another fork that handle that. However, in my use case, user only update data though strapi admin cms, but not though graphql mutation. Is anyone got idea how to solve the problem of cache purge after data update?

To solve cache purge issue, I have override some cache max age by the following code. Strapi graphql cache hint override · GitHub

Then add a plugin in strapi graphql to check if certain variable is in http request, then clear cache entry having expiry time lower than certain value or flush the whole cache for both InMemoryLRUCache and RedisCache. Sample to use apollo-server-plugin-response-cache for Strapi V4 · GitHub

Then write a strapi plugin that give user to simply press a button to fire those variable to strapi graphql in order to clear different level of cache. Hope this help.