All posts
Forward Development

Medusa 2.22.4 caching crashes and the missing graphql dependency

Why @medusajs/caching 2.22.4 throws on untagged caching.set, how to confirm which graphql copy it loads, and how to work around it today.

  • Medusa.js
  • Migrations
  • Operations

You upgrade a Medusa project from 2.22.2 to 2.22.4. The app boots, health checks pass, and nothing unusual appears in the startup log. Then the first code path that caches something without tags fails:

TypeError: Cannot read properties of undefined (reading 'parseObjectForEntities')
    at DefaultCacheStrategy.computeTags (@medusajs/caching/src/utils/strategy.ts:112:36)
    at CachingModuleService.set_ (@medusajs/caching/src/services/cache-module.ts:275:59)
    at CachingModuleService.set (@medusajs/caching/src/services/cache-module.ts:240:23)

If you run with NODE_ENV=production, it's worse: nothing throws. Automatic cache tags just never get computed, so invalidation quietly stops working.

This is medusajs/medusa#17720. It's worth understanding properly. The bug is one version-specific case of a problem that keeps turning up in modular commerce stacks: a package imports something it never declares.

The caching module uses a graphql it does not own

@medusajs/caching@2.22.4 imports graphql in src/utils/parser.ts, but its package.json doesn't list graphql as a dependency. So it loads whichever copy the package manager happens to put within reach.

As of 2.22.4, @medusajs/utils depends on graphql@^17.0.2. In 2.22.2 it was ^16.14.2. @medusajs/utils builds the schema that the caching strategy reads, through GraphQLUtils.makeExecutableSchema in CachingModuleService.onApplicationStart. Now suppose your project also has graphql@16 for something else. The issue's example is Payload CMS. In that case pnpm hoists 16.x, and you end up with two copies in one process:

  • the schema is built with graphql 17, through @medusajs/utils
  • the parser's isObjectType comes from graphql 16, the hoisted copy

graphql deliberately refuses to mix objects from different copies of itself. Outside production mode, new CacheInvalidationParser(...) throws Cannot use GraphQLObjectType "Product" from another module or realm.

Why the app boots anyway

The rejection happens inside onApplicationStart, and MedusaModule.onApplicationStart swallows it (packages/core/modules-sdk/src/medusa-module.ts:141). That causes three problems:

  1. The caching strategy never subscribes to the event bus.
  2. Its parser stays undefined.
  3. Every later caching.set call without tags reaches computeTags, which dereferences the missing parser and throws.

Calls that pass explicit tags skip computeTags, which is why some paths keep working and others don't. That partial breakage sends people looking in the wrong places.

With NODE_ENV=production, graphql's realm check is a plain instanceof. Nothing throws, but the parser's type map is empty and no automatic tags are computed. Staging may crash loudly while production degrades without a sound. If your staging and production run with different NODE_ENV values, you can see completely different symptoms from the same install.

Versions confirmed in the issue:

  • Works: @medusajs/*@2.22.2, where utils and the hoisted copy both use graphql 16.
  • Fails: @medusajs/*@2.22.4 on pnpm 10.34.6 and Node 22, with graphql@16.14.2 also in the workspace.
  • Not fixed upstream: as of 2026-11-09, packages/modules/caching/package.json on develop still declares no graphql. No released version fixes this yet.

Confirm which graphql copy your app resolves

Don't guess from the lockfile. Ask Node, from the directory your Medusa server runs in:

node -e "const p=require.resolve('@medusajs/caching/dist/utils/parser');console.log(require(require.resolve('graphql/package.json',{paths:[p]})).version)"
node -e "const p=require.resolve('@medusajs/framework/utils');console.log(require(require.resolve('graphql/package.json',{paths:[p]})).version)"

If the two versions differ, you have the bug. pnpm why graphql or npm ls graphql shows which package pulled in the second copy.

The issue also gives a direct reproduction:

const { GraphQLUtils } = require("@medusajs/framework/utils")
const { CacheInvalidationParser } = require("@medusajs/caching/dist/utils/parser")
new CacheInvalidationParser(
  GraphQLUtils.makeExecutableSchema({ typeDefs: "type Product { id: ID! }" }),
  []
)

Run it without NODE_ENV=production. A realm error confirms the diagnosis.

Work around it per package manager

The right upstream fix is for @medusajs/caching to declare "graphql": "^17.0.2", the same range as @medusajs/utils. Until that ships, add the declaration yourself.

pnpm. Use packageExtensions. The issue author confirms this fixes it:

{
  "pnpm": {
    "packageExtensions": {
      "@medusajs/caching@2.22.4": {
        "dependencies": { "graphql": "^17.0.2" }
      }
    }
  }
}

You can put the same block in pnpm-workspace.yaml. Reinstall afterwards, then rerun the resolution check above.

Yarn (Berry). .yarnrc.yml supports the same idea:

packageExtensions:
  "@medusajs/caching@2.22.4":
    dependencies:
      graphql: "^17.0.2"

npm. npm has no equivalent. overrides can change the version of a dependency a package already declares, but it can't add a missing one. The practical lever is hoisting. Declare graphql@^17.0.2 in the Medusa app's own package.json so the top-level copy is 17, and let npm nest graphql 16 under whatever needs it. Check the result with the resolution commands, because npm's layout depends on the whole tree.

Keep the version pin in the extension key. When a fixed release lands, the extension stops matching and you'll notice it at review time instead of carrying it forever. Staying on 2.22.2 also avoids the bug, but it holds back everything else in the release train, so treat it as a short-term fallback.

If you would rather not install CMS tooling into the Medusa server package at all, splitting the CMS into its own workspace package with its own graphql is the cleaner long-term layout.

The same pattern exists outside Medusa

An undeclared dependency works or breaks depending on node_modules layout, and that is a property of the ecosystem, not of one package. graphql is especially unforgiving because it rejects objects from a second copy of itself.

Vendure runs on NestJS and its GraphQL layer, and storefronts, CMSs and codegen tools bring their own graphql ranges. Put them in one workspace and you can get the same two-copies-in-one-process failure. We see this as a standing risk in any migration from Magento 2 to Medusa or Vendure. The new stack is a collection of independently versioned packages, and the install layout is now part of your runtime.

Catch undeclared dependencies in CI

Three checks would have caught this before production:

  1. Strict install job. Run a CI install with hoist=false (pnpm) or Plug'n'Play (Yarn). An undeclared import then fails at resolve time instead of picking up a neighbour's copy. You don't have to ship this layout. It's a detector.
  2. Single-copy assertion. Fail the build if more than one major version of graphql can be reached from your server's entry points. Use the resolution one-liners above, or parse npm ls graphql --json.
  3. Boot smoke test that exercises cache tagging. Start the app with NODE_ENV unset, call caching.set without tags, and assert that tags were computed. The startup error is swallowed and production mode hides the symptom entirely, so only a test that exercises the behaviour will catch it.

Until upstream logs the error from CachingModuleService.onApplicationStart, as the issue suggests, run that smoke test on every Medusa minor bump.

If you're planning a move to Medusa or Vendure and want this kind of release discipline built in from the start, get in touch.

Need this done on a real stack?

Magento 2, Adobe Commerce, migrations to Medusa.js or Vendure, enterprise Next.js, WordPress, and AI automation.

Contact us