v2
Guides
Resolving Subscription DataLoader Caching Issues

Resolving Subscription DataLoader Caching Issues

DataLoader is a great utility for reducing database read and HTTP requests to third-party services.

Unfortunately, there is a flaw when using DataLoader with GraphQL subscriptions.

The context object on which all the DataLoader instances should be attached upon is not created per emitted subscription event, but only once when setting up the subscription.

This can lead to huge memory footprints for long-living subscriptions with lots of values being emitted and even worse to sending stale data to the clients as the DataLoader cache is hit although the underlying database record might have changed.

This is a known issue that still has no solution in GraphQL.js core, although, many community members tried to propose solutions with working pull requests that alter the subscribe function.

Previous workarounds have been to disable the DataLoader caching completely or manually calling the DataLoader.clearAll within the mapping of the event values.

import { GraphQLObjectType } from 'graphql'
import pipe from 'lodash/fp/pipe'
import { GraphQLNonNull, GraphQLUserListUpdate } from './GraphQLUserListUpdate'
 
const mapAsyncIterable = <T, O>(map: (input: T) => Promise<O> | O) =>
  async function* mapGenerator(asyncIterable: AsyncIterableIterator<T>) {
    for await (const value of asyncIterable) {
      yield map(value)
    }
  }
 
const GraphQLSubscriptionType = new GraphQLObjectType({
  name: 'Subscription',
  fields: {
    something: {
      type: GraphQLNonNull(GraphQLUserListUpdate),
      resolve: (_, __, context) =>
        pipe(
          context.pubSub.subscribe('userUpdate'),
          mapAsyncIterable(event => {
            context.loader.userLoader.clearAll()
            return event
          })
        )
    }
  }
})

As your project scales this, however, can become a tedious task. With the useContextValuePerExecuteSubscriptionEvent plugin we abstracted this away by having a generic solution for extending the original context with a new part before the subscription event is being executed.

import { envelop } from '@envelop/core'
import { useContextValuePerExecuteSubscriptionEvent } from '@envelop/execute-subscription-event'
import { createContext, createDataLoaders } from './context'
 
const getEnveloped = envelop({
  plugins: [
    // ... other plugins
    useContext(() => createContext()),
    useContextValuePerExecuteSubscriptionEvent(() => ({
      // Existing context is merged with this context partial
      contextPartial: {
        loader: createDataLoaders()
      }
    }))
  ]
})

With this easy fix, new DataLoader instances are used for every published value!

Learn more on Plugin Hub.