npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

graphql-query-complexity-esm

v2.0.3

Published

GraphQL query complexity analysis validation rule with estimator chains, directive support, and iterative engine

Readme

graphql-query-complexity-esm

CI Publish Pages npm version npm downloads License: MIT Node >=20 Provenance

A deeply nested or fan-out GraphQL query can burn through resources that simple rate limits won't catch. graphql-query-complexity-esm scores every field and rejects queries over budget before a single resolver runs.

  • Validation rule (complexityLimit): plugs into any server's validate pipeline
  • Programmatic API (getComplexity, getComplexityBreakdown): analyze costs outside validation
  • Estimator chains: first estimator returning a finite number wins; return undefined to defer
  • Built-in estimators: simpleEstimator (flat cost) and fieldExtensionsEstimator (@complexity directive / field.extensions.complexity)
  • Directive-aware: honors @skip and @include
  • Fragment support: named fragments, inline fragments, per-path cycle protection
  • Node-count guard: configurable maxNodes (default 10_000) prevents AST explosion
  • Typed error codes: ESTIMATOR_ERROR, NODE_LIMIT_EXCEEDED, QUERY_TOO_COMPLEX
  • TypeScript: ships .d.ts declarations, works with plain JS too
  • ESM + CJS dual publish

Interactive Demo

Try it live or run locally:

cd examples/visualization
npm install
npm run dev

Preset queries (simple lookups through exponential fan-out), an animated scan showing per-field costs, and a detail panel for inspecting cost formulas.

Requirements

  • Node.js >=20.0.0
  • Peer dependency: graphql ^16.0.0

Installation

npm install graphql-query-complexity-esm graphql
pnpm add graphql-query-complexity-esm graphql
yarn add graphql-query-complexity-esm graphql

Quickstart

import { buildSchema, parse, specifiedRules, validate } from "graphql";
import { complexityLimit, simpleEstimator } from "graphql-query-complexity-esm";

const schema = buildSchema(`
  type Query {
    users(limit: Int): [User!]!
  }
  type User {
    id: ID!
    name: String!
  }
`);

const rule = complexityLimit(1000, {
  estimators: [simpleEstimator({ defaultComplexity: 1 })],
  variables: {},
});

const document = parse(`query { users(limit: 10) { id name } }`);
const errors = validate(schema, document, [...specifiedRules, rule]);

if (errors.length > 0) {
  console.error("Query rejected:", errors[0].message);
}

API Reference

complexityLimit(maxComplexity, options?, callback?)

Returns a validation rule that rejects queries over the given complexity score.

maxComplexity: required, positive integer.

Options:

| Option | Type | Default | Validation | |---|---|---|---| | defaultComplexity | number | 1 | Non-negative integer | | estimators | ComplexityEstimator[] | [simpleEstimator({ defaultComplexity })] | Non-empty array of functions | | maxNodes | number | 10_000 | Positive integer | | variables | Record<string, unknown> | {} | Plain object |

Callback:

Optional ComplexityCallback, called on document leave when no error was reported. Receives a ComplexityByOperation map (operation name to score).

Anonymous operations get deterministic keys: "[anonymous]", then "[anonymous:2]", "[anonymous:3]", etc.

const rule = complexityLimit(
  1000,
  {
    estimators: [fieldExtensionsEstimator(), simpleEstimator({ defaultComplexity: 1 })],
    variables: request.variables ?? {},
  },
  (complexities) => {
    for (const [name, cost] of Object.entries(complexities)) {
      console.log(`${name}: ${cost}`);
    }
  },
);

getComplexity(options) / getComplexityBreakdown(options)

Calculate complexity outside the validation pipeline.

| Option | Type | Required | Default | Validation | |---|---|---|---|---| | estimators | ComplexityEstimator[] | yes | — | Non-empty array of functions | | query | string \| DocumentNode | yes | — | String or DocumentNode | | schema | GraphQLSchema | yes | — | GraphQL schema instance | | maxNodes | number | no | 10_000 | Positive integer | | variables | Record<string, unknown> | no | {} | Plain object |

  • getComplexity() returns the highest score across all operations.
  • getComplexityBreakdown() returns a frozen ComplexityByOperation map.

Both throw QueryComplexityValidationError on parse/validation failures:

import {
  getComplexity,
  QueryComplexityValidationError,
  simpleEstimator,
} from "graphql-query-complexity-esm";

try {
  const cost = getComplexity({
    estimators: [simpleEstimator({ defaultComplexity: 1 })],
    query: `{ users { id name } }`,
    schema,
  });
  console.log("Query cost:", cost);
} catch (error) {
  if (error instanceof QueryComplexityValidationError) {
    // error.errors: readonly GraphQLError[]
    // error.message: all messages joined with newline
    console.error(error.errors);
  }
}

fieldExtensionsEstimator()

Reads cost from field.extensions.complexity or the @complexity directive.

Resolution order (first match wins):

  1. field.extensions.complexity as a finite number → value + childComplexity
  2. field.extensions.complexity as { value: number, multipliers?: string[] } → cost formula
  3. @complexity directive on the field definition → cost formula
  4. Returns undefined (defers to the next estimator)

Cost formula:

cost = value + (product of multiplier argument values, default 1) * childComplexity
// Programmatic extensions (code-first schemas)
field.extensions = { complexity: 10 };                              // flat number
field.extensions = { complexity: { value: 2, multipliers: ["limit"] } }; // with multipliers
# Directive (SDL-first schemas) - add complexityDirectiveTypeDefs to your schema
type Query {
  users(limit: Int): [User!]! @complexity(value: 2, multipliers: ["limit"])
}

simpleEstimator(options?)

Fixed base cost per field, plus child complexity.

  • defaultComplexity: base cost per field (default 1)
  • Formula: cost + childComplexity

Note: This estimator does not account for list multipliers. Fields returning lists (e.g. users(limit: 100)) receive the same cost as scalar fields. Use fieldExtensionsEstimator or a custom estimator for accurate list costing.

Custom Estimators

An estimator receives field context and returns a cost (number) or undefined to defer. Evaluated in order: first finite number wins.

import type { ComplexityEstimator } from "graphql-query-complexity-esm";

/** Assigns a higher base cost to fields that return list types. */
const listPenaltyEstimator: ComplexityEstimator = ({
  childComplexity,
  field,
  type,
}) => {
  const returnType = field.type;
  const isList = returnType.toString().startsWith("[");
  if (!isList) return undefined; // defer to next estimator

  return 5 + childComplexity;
};

// Chain with built-in estimators (first match wins):
const estimators = [
  fieldExtensionsEstimator(), // check extensions/directives first
  listPenaltyEstimator,       // then apply list penalty
  simpleEstimator(),           // fallback: 1 per field
];

complexityDirectiveTypeDefs

SDL string for the @complexity directive. Include in your schema when using directive-based costs with fieldExtensionsEstimator:

directive @complexity(value: Int!, multipliers: [String!]) on FIELD_DEFINITION

ERROR_CODES

Frozen object with GraphQL error extension codes:

| Code | Trigger | |---|---| | ESTIMATOR_ERROR | An estimator threw during evaluation | | NODE_LIMIT_EXCEEDED | Query exceeded maxNodes | | QUERY_TOO_COMPLEX | Query exceeded maxComplexity |

Exports

Runtime:

| Export | Description | |---|---| | complexityDirectiveTypeDefs | SDL for the @complexity directive | | complexityLimit | Validation rule factory | | ERROR_CODES | Error extension codes | | fieldExtensionsEstimator | Extension/directive-based estimator | | getComplexity | Programmatic max complexity | | getComplexityBreakdown | Programmatic per-operation breakdown | | QueryComplexityValidationError | Error class for validation failures | | simpleEstimator | Fixed-cost estimator |

Types:

| Export | Description | |---|---| | CoercionErrorInfo | Coercion error details for directive arguments | | ComplexityByOperation | Operation-name → complexity map | | ComplexityCallback | Callback signature | | ComplexityEstimator | Estimator function signature | | ComplexityEstimatorArgs | Arguments passed to estimators | | ComplexityExtensionConfig | { value, multipliers? } shape | | ComplexityLimitFunction | Overloaded complexityLimit signature | | ComplexityLimitOptions | Options for complexityLimit | | ComplexityResult | Result object from the complexity engine | | ComplexityViolation | Violation details for over-budget operations | | GetComplexityOptions | Options for getComplexity* |

Integration Examples

Apollo Server

Checks complexity in didResolveOperation and rejects before execution. Full example

import { GraphQLError } from "graphql";
import {
  fieldExtensionsEstimator,
  getComplexity,
  simpleEstimator,
} from "graphql-query-complexity-esm";

const MAX_COMPLEXITY = 1000;

// Inside ApolloServer config:
const server = new ApolloServer({
  plugins: [
    {
      async requestDidStart({ schema }) {
        return {
          async didResolveOperation({ document, request }) {
            const complexity = getComplexity({
              estimators: [fieldExtensionsEstimator(), simpleEstimator({ defaultComplexity: 1 })],
              query: document,
              schema,
              variables: request.variables ?? {},
            });

            if (complexity > MAX_COMPLEXITY) {
              throw new GraphQLError(
                `Query complexity ${complexity} exceeds maximum of ${MAX_COMPLEXITY}.`,
                {
                  extensions: {
                    code: "QUERY_TOO_COMPLEX",
                    complexity,
                    maximumComplexity: MAX_COMPLEXITY,
                  },
                },
              );
            }
          },
        };
      },
    },
  ],
  // ...
});

GraphQL Yoga

Passes complexityLimit() as a validation rule through onValidate. Full example

import {
  complexityLimit,
  fieldExtensionsEstimator,
  simpleEstimator,
} from "graphql-query-complexity-esm";

const MAX_COMPLEXITY = 1000;

// Inside createYoga config:
const yoga = createYoga({
  plugins: [
    {
      onValidate({ addValidationRule, params }) {
        const variables = (params.variables as Record<string, unknown> | undefined) ?? {};

        addValidationRule(
          complexityLimit(MAX_COMPLEXITY, {
            estimators: [fieldExtensionsEstimator(), simpleEstimator({ defaultComplexity: 1 })],
            variables,
          }),
        );
      },
    },
  ],
  // ...
});

Performance

Complexity analysis uses an iterative DFS engine with explicit stack management — no recursion, no stack overflow risk, constant overhead regardless of query depth.

Benchmark thresholds enforced in CI (Node 22, single core):

| Scenario | Query shape | Max allowed | |---|---|---| | Small | 2-field flat query | ≤ 6 ms | | Medium | Multi-level nested with list arguments | ≤ 6 ms | | Deep | 18 levels of recursive nesting (complexity 262 144) | ≤ 9 ms | | Wide | 250 aliased field selections | ≤ 15 ms | | Heavy | Users → Posts → Comments → Author hierarchy | ≤ 6 ms |

Run benchmarks locally:

pnpm run bench          # table output
pnpm run bench:check    # regression check against thresholds
pnpm run bench:json     # JSON output for CI integration

Troubleshooting

| Symptom | Cause | Fix | |---|---|---| | RangeError: maxComplexity must be a positive integer | Invalid first argument to complexityLimit | Pass a positive integer | | RangeError / TypeError from option validation | Invalid option types | Verify estimators, maxNodes, variables shapes | | QueryComplexityValidationError thrown | Parse or validation failure in getComplexity* | Inspect error.errors for GraphQL error details | | Extension code QUERY_TOO_COMPLEX | Complexity exceeded maxComplexity | Increase limit or tune estimators | | Extension code NODE_LIMIT_EXCEEDED | Query exceeded maxNodes | Increase maxNodes or reduce query breadth/depth | | Extension code ESTIMATOR_ERROR | An estimator threw | Guard estimator logic against unexpected arguments | | @skip/@include with missing variables | Directive coercion failure | Nodes are treated as included; pass all required variables |

Development

Build:

| Script | Command | |---|---| | build | tsup | | dev | tsc --watch |

Lint:

| Script | Command | |---|---| | lint | biome check && tsc --noEmit | | lint:fix | biome check --write && tsc --noEmit |

Test:

| Script | Command | |---|---| | test | vitest run | | test:coverage | vitest run --coverage | | test:ui | vitest --ui | | test:watch | vitest |

Benchmark:

| Script | Command | |---|---| | bench | tsx scripts/benchmark.ts | | bench:check | tsx scripts/benchmark-check.ts | | bench:json | tsx scripts/benchmark.ts --json |

Examples:

| Script | Command | |---|---| | example:apollo | tsx examples/servers/apollo-server.ts | | example:yoga | tsx examples/servers/yoga-server.ts |

| Module | Purpose | |---|---| | src/index.ts | Public runtime and type exports | | src/complexity-rule.ts | complexityLimit factory, input validation, callback dispatch, error reporting | | src/complexity-engine.ts | Iterative traversal engine, estimator execution, fragment processing, node counting | | src/get-complexity.ts | Programmatic wrapper around validate() + complexityLimit() | | src/estimators.ts | simpleEstimator and fieldExtensionsEstimator | | src/directives.ts | complexityDirectiveTypeDefs, shouldSkipNode (@skip/@include) | | src/constants.ts | DEFAULT_MAX_NODES, ERROR_CODES | | src/types.ts | Public interfaces, types, and QueryComplexityValidationError | | src/utils.ts | Internal shared helpers (null-prototype records, type guards, value descriptions) | | src/__tests__/ | Behavior tests for all modules |

Build output: src/index.tstsupdist/ (ESM + CJS + .d.ts + sourcemaps)

pnpm bench

CLI arguments (scripts/benchmark.ts):

| Argument | Description | |---|---| | --json | Output results as JSON | | --output <path> | Write results to file | | --scale <number> | Iteration scale factor | | --warmup <integer> | Number of warmup runs |

Regression check (pnpm bench:check) env vars:

| Variable | Default | Purpose | |---|---|---| | BENCHMARK_THRESHOLDS_PATH | benchmarks/thresholds.json | Path to thresholds JSON | | BENCH_ITERATIONS_SCALE | Thresholds file value, then 0.45 | Iteration scale override | | BENCH_WARMUP_RUNS | Thresholds file value, then 30 | Warmup runs override |

Related Packages

This package is part of a suite of GraphQL security tools that work independently or together to protect your API:

| Package | Purpose | |---|---| | graphql-query-depth-limit-esm | Rejects deeply nested queries before execution | | graphql-rate-limit-redis-esm | Redis-backed per-field rate limiting via @rateLimit directive |

Recommended layering: Use depth limiting as a fast, cheap first gate, complexity analysis for fine-grained cost control, and rate limiting for per-client throttling.

License

MIT