Safeguard
Open Source

The dataloader npm Package: Security Review and Safe Usage

The dataloader npm package batches and caches data fetches, most often in GraphQL servers. Its security story is less about CVEs and more about cache scoping and how you use it.

Marcus Chen
DevSecOps Engineer
6 min read

The dataloader npm package is a small batching-and-caching utility, and its most important security consideration is not a vulnerability in the library itself but how you scope its cache — get that wrong and one user can see another user's data. DataLoader solves the classic GraphQL "N+1 query" problem by coalescing many individual loads into a single batched request, and it memoizes results within its lifetime. That memoization is exactly where teams get burned.

If you run a GraphQL server in Node, the npm dataloader package is almost certainly somewhere in your stack. It is a foundational building block, and it does its narrow job well. The risk is architectural, not a defect.

What DataLoader does

DataLoader wraps a batch-loading function. Instead of calling your database once per record, you call loader.load(id) many times, and DataLoader collects those calls within a single tick of the event loop, then invokes your batch function once with all the keys.

const DataLoader = require("dataloader");

const userLoader = new DataLoader(async (ids) => {
  const rows = await db.users.findByIds(ids);
  // must return results in the same order as ids
  return ids.map((id) => rows.find((r) => r.id === id) || null);
});

const a = userLoader.load("1");
const b = userLoader.load("2");
// one batched db call, not two

Two things happen here: batching (many loads become one query) and caching (a repeated load for the same key returns the memoized promise). Both are useful. The caching is the part with a security dimension.

The cross-request data-leak risk

This is the single most important thing to understand about the npm dataloader package. Its cache is a plain in-memory map keyed by the values you pass to load. If you create one DataLoader instance and share it across every incoming request, you have built a cache that persists between users.

Consider an authorization-aware loader. Request A, authenticated as user Alice, loads record 42, which Alice is allowed to see. The result is now memoized in the shared instance. Request B, authenticated as Bob, loads record 42 — and DataLoader returns the memoized value without ever consulting your batch function, so Bob's authorization is never checked. Bob just read Alice's data.

The official guidance is explicit and worth repeating: create a new set of DataLoaders per request. DataLoader is designed to be short-lived. Instantiate the loaders inside your GraphQL context factory so each request gets a fresh cache that is discarded when the request ends.

// per-request context
function createContext(req) {
  return {
    user: req.user,
    loaders: {
      user: new DataLoader(batchUsers),
      post: new DataLoader(batchPosts)
    }
  };
}

With this pattern the cache never outlives a single request, and cross-request leakage becomes impossible. The batching benefit — the whole reason you reached for DataLoader — is fully preserved within the request, which is where nearly all the N+1 explosions happen anyway.

Authorization belongs outside the cache

Even per-request, do not put authorization logic inside the batch function in a way that the cache can skip. If the same key can resolve to "allowed" or "denied" depending on who is asking, the cache key is wrong. Either include the identity in the cache key, or — cleaner — enforce authorization at the resolver layer before you ever call load, and let the loader deal only with raw data fetching. Keep the loader dumb and the policy explicit.

Supply-chain and maintenance considerations

DataLoader is a mature, widely-used package with a very small footprint and minimal dependencies of its own. A tiny dependency tree is a genuine security advantage: fewer transitive packages means a smaller surface for a supply-chain compromise. That said, "small and popular" is not "immune," and the general npm hygiene rules still apply.

Pin the version with a lockfile and reproduce installs with npm ci so you do not silently pick up a new release. When you upgrade, read the changelog rather than trusting a range. And keep an eye on the whole tree, not just this one package — a compromise is far more likely to arrive through a large, sprawling dependency than through a focused utility like this. An SCA tool such as Safeguard can watch every version in the tree and flag a known-bad release the moment an advisory lands, which matters more for your fat dependencies than for dataloader itself. See our software composition analysis overview for how transitive coverage works.

Denial-of-service through unbounded caching

There is a subtler resource issue. If you ever do use a long-lived DataLoader deliberately (for genuinely immutable reference data, for example), its cache grows without bound by default. An attacker who can influence the keys — say, by requesting many distinct IDs — can push memory usage up. For any loader that outlives a request, set a bounded cache or a custom cache map with eviction rather than the default unbounded map. For the standard per-request pattern this is a non-issue, because the cache is thrown away when the request completes.

A safe-usage checklist

  • Instantiate loaders per request, inside your context factory, never as module-level singletons.
  • Enforce authorization at the resolver, before calling load, or bake identity into the cache key.
  • Return batch results in the exact order of the input keys — mismatches silently return the wrong record for the wrong key, which is a correctness and potentially a data-exposure bug.
  • Pin the version and reproduce installs with a lockfile.
  • Bound the cache for any deliberately long-lived loader.

Most production incidents involving the npm dataloader package trace back to the first two items. The library is not the problem; a shared cache crossing a trust boundary is.

FAQ

Is the dataloader npm package safe to use?

Yes. It is a mature, minimal, widely-used utility. Its security risk is architectural — sharing a cache across requests can leak data between users — not a defect in the library. Instantiate loaders per request and you avoid the main pitfall.

Why should DataLoader be created per request?

Because its cache persists for the lifetime of the instance. A shared instance memoizes results across users, so a later request can receive a cached value without its own authorization being checked. Per-request instances discard the cache when the request ends.

Does DataLoader check authorization?

No. It only batches and caches data fetches. Authorization must be enforced separately, ideally at the resolver before you call load, so the cache can never bypass a permission check.

Can DataLoader cause a memory leak?

Only if you use a long-lived instance with the default unbounded cache. The recommended per-request pattern avoids this entirely because the cache is garbage-collected when the request finishes. For deliberately long-lived loaders, configure a bounded cache.

Never miss an update

Weekly insights on software supply chain security, delivered to your inbox.