Skip to main content

A python library that provides complexity calculation helpers for GraphQL

Project description

GraphQL Complexity Logo

GraphQL Complexity

Welcome to GraphQL-Complexity! This Python library provides functionality to compute the complexity of a GraphQL operation, contributing to better understanding and optimization of your GraphQL APIs. This library is designed to be stable, robust, and highly useful for developers working with GraphQL.

Build PyPI codecov Downloads Python Version License: MIT

✨ Why GraphQL Complexity?

Protect your GraphQL API from expensive queries and potential DoS attacks by calculating query complexity before execution.

Watch the library live in our demo playground here .

Features

  • Compute complexity of GraphQL queries
  • Multiple built-in estimators for complexity computation
  • Customizable estimators for specific use cases
  • Support for Strawberry GraphQL library

Table of Contents

Installation (Quick Start)

You can install the library via pip:

pip install graphql-complexity

For Strawberry GraphQL integration, use the following command:

pip install graphql-complexity[strawberry-graphql]

Getting Started

Create a file named complexity.py with the following content:

from graphql_complexity import get_complexity, SimpleEstimator
from graphql import build_schema


schema = build_schema("""
    type User {
        id: ID!
        name: String!
    }
    type Query {
        user: User
    }
""")

query = """
    query SomeQuery {
        user {
            id
            name
        }
    }
"""

complexity = get_complexity(
    query=query, 
    schema=schema,
    estimator=SimpleEstimator(complexity=10)
)
if complexity > 10:
    raise Exception("Query is too complex")

The library exposes the method get_complexity with the algorithm to compute the complexity of a GraphQL operation. The algorithm visits each node of the query and computes the complexity of each field using an estimator.

Understanding Complexity Calculations with explain_complexity

The library provides an explain_complexity function that offers detailed insights into how complexity is calculated. This debugging tool returns the inner details that led to the final complexity value, making it invaluable for:

  • Debugging expensive queries - Identify which fields contribute most to complexity
  • Comparing different queries - Understand why one query is more expensive than another
  • Learning the library - See exactly how the estimator evaluates each field
  • Tuning estimators - Fine-tune your complexity configuration
from graphql_complexity import explain_complexity, SimpleEstimator
from graphql import build_schema

schema = build_schema("""
    type User {
        id: ID!
        name: String!
        posts: [Post!]!
    }
    type Post {
        id: ID!
        title: String!
    }
    type Query {
        user: User
    }
""")

query = """
    query {
        user {
            id
            name
            posts {
                id
                title
            }
        }
    }
"""

explanation = explain_complexity(
    query=query,
    schema=schema,
    estimator=SimpleEstimator(complexity=5)
)

print(explanation)

Example Output:

================================================================================
GraphQL Complexity Explanation
================================================================================

Total Complexity: 35

Estimator Used:
  Name: SimpleEstimator
  Details:
    complexity_constant: 5

Complexity Tree:
--------------------------------------------------------------------------------
root (RootNode) = 35
	user (Field) = 35
		id (Field) = 5
		name (Field) = 5
		posts (ListField) = 25
			id (Field) = 5
			title (Field) = 5

Field-by-Field Breakdown:
--------------------------------------------------------------------------------
user (Field)
  Field complexity: 5
  Children complexity: 30
  Total: 35

user.id (Field)
  Field complexity: 5
  Children complexity: 0
  Total: 5

user.name (Field)
  Field complexity: 5
  Children complexity: 0
  Total: 5

user.posts (ListField) [multiplier: 1]
  Field complexity: 5
  Children complexity: 10
  Total: 25

user.posts.id (Field)
  Field complexity: 5
  Children complexity: 0
  Total: 5

user.posts.title (Field)
  Field complexity: 5
  Children complexity: 0
  Total: 5

================================================================================

The explanation shows:

  • Total Complexity: The final calculated value
  • Estimator Details: Which estimator was used and its configuration
  • Complexity Tree: A hierarchical view of how complexity accumulates
  • Field Breakdown: Per-field analysis showing how each field contributes to the total

Estimators

GraphQL-Complexity provides various built-in estimators for computing query complexity:

SimpleEstimator

Estimate fields complexity based on constants for complexity and multiplier. This assigns a constant complexity value to each field and multiplies it by another constant, which is propagated along the depth of the query.

from graphql_complexity import SimpleEstimator


estimator = SimpleEstimator(complexity=2)

DirectivesEstimator

Define fields complexity using schema directives. This assigns a complexity value to each field and multiplies it by the depth of the query. It also supports the @complexity directive to assign a custom complexity value to a field.

from graphql_complexity import DirectivesEstimator


schema = """
directive @complexity(
  value: Int!
) on FIELD_DEFINITION

type Query {
  oneField: String @complexity(value: 5)
  otherField: String @complexity(value: 1)
  withoutDirective: String
}
"""

estimator = DirectivesEstimator(schema)

ArgumentsEstimator

Estimate complexity by multiplying a base value by a numeric argument (e.g. limit, first, ids). This is useful when your API exposes pagination or batch arguments that directly drive the cost of a field.

  • Int argument — the integer value is used as the multiplier (limit: 10 → ×10).
  • List argument — the length of the list is used as the multiplier (ids: ["a","b","c"] → ×3).
  • No matching argument — the field gets default_complexity (multiplier of 1).
from graphql_complexity import ArgumentsEstimator, get_complexity
from graphql import build_schema

schema = build_schema("""
    type Query {
        users(limit: Int): [User!]!
        usersByIds(ids: [ID!]): [User!]!
    }
    type User {
        id: ID!
        name: String!
    }
""")

estimator = ArgumentsEstimator(
    multipliers=["limit", "ids"],
    default_complexity=1,
)

# users(limit: 50) → complexity 50
complexity = get_complexity(
    query="query { users(limit: 50) { id name } }",
    schema=schema,
    estimator=estimator,
)
print(complexity)  # 52  (users=50, id=1, name=1)

# usersByIds(ids: ["a","b","c"]) → complexity 3
complexity = get_complexity(
    query='query { usersByIds(ids: ["a", "b", "c"]) { id } }',
    schema=schema,
    estimator=estimator,
)
print(complexity)  # 4  (usersByIds=3, id=1)

Custom estimator

Custom estimators can be defined to compute the complexity of a field using the ComplexityEstimator interface.

from graphql_complexity import ComplexityEstimator


class CustomEstimator(ComplexityEstimator):
    def get_field_complexity(self, node, type_info, path) -> int:
        if node.name.value == "specificField":
            return 100
        return 1

Advanced Examples

Handling Fragments

GraphQL fragments allow you to reuse query parts. Here's how complexity is calculated for queries with fragments:

from graphql_complexity import get_complexity, SimpleEstimator
from graphql import build_schema

schema = build_schema("""
    type User {
        id: ID!
        name: String!
        email: String!
        posts: [Post!]!
    }
    
    type Post {
        id: ID!
        title: String!
        content: String!
        author: User!
    }
    
    type Query {
        user(id: ID!): User
        users: [User!]!
    }
""")

# Query with named fragment
query = """
    query GetUsers {
        users {
            ...UserFields
            posts {
                ...PostFields
            }
        }
    }
    
    fragment UserFields on User {
        id
        name
        email
    }
    
    fragment PostFields on Post {
        id
        title
        content
    }
"""

complexity = get_complexity(
    query=query,
    schema=schema,
    estimator=SimpleEstimator(complexity=1)
)
print(f"Query complexity: {complexity}")

Complex Nested Queries

When dealing with nested relationships, complexity can grow exponentially. Here's how to handle it:

from graphql_complexity import get_complexity, DirectivesEstimator
from graphql import build_schema

schema = build_schema("""
    directive @complexity(
        value: Int!
    ) on FIELD_DEFINITION
    
    type Organization {
        id: ID!
        name: String!
        teams: [Team!]! @complexity(value: 5)
    }
    
    type Team {
        id: ID!
        name: String!
        members: [User!]! @complexity(value: 3)
    }
    
    type User {
        id: ID!
        name: String!
        email: String!
        tasks: [Task!]! @complexity(value: 2)
    }
    
    type Task {
        id: ID!
        title: String!
        description: String!
    }
    
    type Query {
        organization(id: ID!): Organization
    }
""")

# Deeply nested query
query = """
    query GetOrgStructure {
        organization(id: "1") {
            id
            name
            teams {
                id
                name
                members {
                    id
                    name
                    email
                    tasks {
                        id
                        title
                        description
                    }
                }
            }
        }
    }
"""

estimator = DirectivesEstimator(schema)
complexity = get_complexity(query=query, schema=schema, estimator=estimator)

# Set a reasonable limit
MAX_COMPLEXITY = 100
if complexity > MAX_COMPLEXITY:
    raise Exception(f"Query too complex: {complexity} > {MAX_COMPLEXITY}")

Working with Arguments

Field arguments can significantly impact query complexity, especially with pagination. The built-in ArgumentsEstimator handles the common case where a numeric argument (like first or limit) or a list argument (like ids) directly drives the cost of a field:

from graphql_complexity import ArgumentsEstimator, get_complexity
from graphql import build_schema

schema = build_schema("""
    type Product {
        id: ID!
        name: String!
        price: Float!
    }

    type Query {
        products(first: Int, offset: Int): [Product!]!
        product(id: ID!): Product
    }
""")

estimator = ArgumentsEstimator(multipliers=["first"], default_complexity=1)

query = """
    query GetProducts {
        products(first: 50) {
            id
            name
            price
        }
    }
"""

complexity = get_complexity(query=query, schema=schema, estimator=estimator)
print(f"Query complexity with pagination: {complexity}")
# products(first: 50) → 50, id/name/price → 1 each → total: 53

For more fine-grained control (e.g. capping limits or applying field-specific multipliers), use a Custom Estimator instead.

Combining Multiple Estimators

You can create sophisticated complexity analysis by combining estimators:

from graphql_complexity import (
    ComplexityEstimator,
    SimpleEstimator,
    DirectivesEstimator,
    get_complexity
)
from graphql import build_schema

schema = build_schema("""
    directive @complexity(value: Int!) on FIELD_DEFINITION
    
    type User {
        id: ID!
        name: String!
        expensiveComputation: String! @complexity(value: 50)
        posts: [Post!]!
    }
    
    type Post {
        id: ID!
        title: String!
    }
    
    type Query {
        user(id: ID!): User
        users: [User!]!
    }
""")

class CompositeEstimator(ComplexityEstimator):
    """Combines multiple estimation strategies"""
    
    def __init__(self):
        self.directive_estimator = DirectivesEstimator(schema)
        self.simple_estimator = SimpleEstimator(complexity=1)
    
    def get_field_complexity(self, node, type_info, path) -> int:
        # First, try directive-based estimation
        directive_complexity = self.directive_estimator.get_field_complexity(
            node, type_info, path
        )
        
        # If directive provides a value, use it
        if directive_complexity > 1:
            return directive_complexity
        
        # Otherwise, fall back to simple estimation
        return self.simple_estimator.get_field_complexity(node, type_info, path)

query = """
    query GetUserData {
        user(id: "1") {
            id
            name
            expensiveComputation
            posts {
                id
                title
            }
        }
    }
"""

complexity = get_complexity(
    query=query,
    schema=schema,
    estimator=CompositeEstimator()
)
print(f"Combined estimator complexity: {complexity}")

Framework Integration

Strawberry GraphQL

The library is compatible with strawberry-graphql. Use the following command to install the library with Strawberry support:

poetry install --extras strawberry-graphql

To use the library with Strawberry GraphQL, use the build_complexity_extension method to build the complexity extension and add it to the schema. This method receives an estimator and returns a complexity extension that can be added to the schema.

import strawberry
from graphql_complexity import SimpleEstimator
from graphql_complexity.extensions.strawberry_graphql import build_complexity_extension


@strawberry.type
class Query:
    @strawberry.field()
    def hello_world(self) -> str:
        return "Hello world!"

extension = build_complexity_extension(estimator=SimpleEstimator())
schema = strawberry.Schema(query=Query, extensions=[extension])

schema.execute_sync("query { helloWorld }")

The build_complexity_extension method accepts an estimator as optional argument giving the possibility to use one of the built-in estimators or a custom estimator.

Django Integration

Integrate complexity analysis with Django and Graphene:

# myapp/complexity.py
from graphql_complexity import get_complexity, SimpleEstimator
from graphql import GraphQLError


class ComplexityMiddleware:
    def __init__(self, max_complexity=1000):
        self.max_complexity = max_complexity
        self.estimator = SimpleEstimator(complexity=1)
    
    def resolve(self, next, root, info, **args):
        # Calculate complexity on first field resolution
        if not hasattr(info.context, '_complexity_checked'):
            try:
                complexity = get_complexity(
                    query=info.operation.loc.source.body,
                    schema=info.schema,
                    estimator=self.estimator
                )
                
                if complexity > self.max_complexity:
                    raise GraphQLError(
                        f"Query is too complex: {complexity}. "
                        f"Maximum allowed complexity: {self.max_complexity}"
                    )
                
                info.context._complexity_checked = True
                info.context._query_complexity = complexity
                
            except Exception as e:
                raise GraphQLError(f"Complexity analysis failed: {str(e)}")
        
        return next(root, info, **args)


# settings.py
GRAPHENE = {
    'MIDDLEWARE': [
        'myapp.complexity.ComplexityMiddleware',
    ],
}

FastAPI Integration

Use complexity analysis with FastAPI and Strawberry:

from fastapi import FastAPI
from strawberry.fastapi import GraphQLRouter
import strawberry
from graphql_complexity import SimpleEstimator
from graphql_complexity.extensions.strawberry_graphql import build_complexity_extension


@strawberry.type
class User:
    id: strawberry.ID
    name: str
    email: str


@strawberry.type
class Query:
    @strawberry.field
    def user(self, id: strawberry.ID) -> User:
        return User(id=id, name="John Doe", email="john@example.com")
    
    @strawberry.field
    def users(self) -> list[User]:
        return [
            User(id="1", name="John Doe", email="john@example.com"),
            User(id="2", name="Jane Smith", email="jane@example.com"),
        ]


# Configure complexity limit
MAX_COMPLEXITY = 1000
extension = build_complexity_extension(
    estimator=SimpleEstimator(complexity=1),
    max_complexity=MAX_COMPLEXITY
)

schema = strawberry.Schema(query=Query, extensions=[extension])
graphql_app = GraphQLRouter(schema)

app = FastAPI()
app.include_router(graphql_app, prefix="/graphql")

Flask Integration

Integrate with Flask-GraphQL:

from flask import Flask, request, jsonify
from flask_graphql import GraphQLView
from graphql import build_schema, GraphQLError
from graphql_complexity import get_complexity, SimpleEstimator


app = Flask(__name__)

schema = build_schema("""
    type User {
        id: ID!
        name: String!
        email: String!
    }
    
    type Query {
        user(id: ID!): User
        users: [User!]!
    }
""")

MAX_COMPLEXITY = 1000
estimator = SimpleEstimator(complexity=1)


def validate_complexity(query_string):
    """Validate query complexity before execution"""
    try:
        complexity = get_complexity(
            query=query_string,
            schema=schema,
            estimator=estimator
        )
        
        if complexity > MAX_COMPLEXITY:
            raise GraphQLError(
                f"Query exceeds complexity limit. "
                f"Query complexity: {complexity}, Max allowed: {MAX_COMPLEXITY}"
            )
        
        return complexity
    except Exception as e:
        raise GraphQLError(f"Complexity validation error: {str(e)}")


class ComplexityGraphQLView(GraphQLView):
    def dispatch_request(self):
        # Get the query from request
        data = request.get_json()
        query = data.get('query', '')
        
        # Validate complexity
        try:
            complexity = validate_complexity(query)
            # Store complexity in request context for logging
            request.query_complexity = complexity
        except GraphQLError as e:
            return jsonify({'errors': [{'message': str(e)}]}), 400
        
        # Proceed with normal GraphQL execution
        return super().dispatch_request()


app.add_url_rule(
    '/graphql',
    view_func=ComplexityGraphQLView.as_view(
        'graphql',
        schema=schema,
        graphiql=True
    )
)


if __name__ == '__main__':
    app.run(debug=True)

🍳 Recipes

1. Reject a query before execution

The most common pattern — block expensive queries before they hit your resolvers.

from graphql_complexity import get_complexity, SimpleEstimator

MAX_COMPLEXITY = 10

def execute_query(schema, query):
    complexity = get_complexity(
        query=query,
        schema=schema,
        estimator=SimpleEstimator(complexity=1)
    )

    if complexity > MAX_COMPLEXITY:
        raise Exception(f"Query too complex ({complexity}). Max allowed: {MAX_COMPLEXITY}")

    return schema.execute(query)

2. Set different limits per user role

Give trusted users more headroom without opening the door to abuse.

COMPLEXITY_LIMITS = {
    "anonymous": 5,
    "authenticated": 15,
    "admin": 50,
}

def execute_query(schema, query, user_role="anonymous"):
    complexity = get_complexity(
        query=query,
        schema=schema,
        estimator=SimpleEstimator(complexity=1)
    )

    limit = COMPLEXITY_LIMITS.get(user_role, 5)

    if complexity > limit:
        raise Exception(f"Query too complex ({complexity}). Max allowed for {user_role}: {limit}")

    return schema.execute(query)

3. Log complexity without blocking

Useful when rolling out complexity limits gradually — monitor first, enforce later.

import logging
from graphql_complexity import get_complexity, SimpleEstimator

logger = logging.getLogger(__name__)
WARN_THRESHOLD = 10

def execute_query(schema, query):
    complexity = get_complexity(
        query=query,
        schema=schema,
        estimator=SimpleEstimator(complexity=1)
    )

    if complexity > WARN_THRESHOLD:
        logger.warning(f"High complexity query detected: {complexity}")

    # Always execute — no blocking yet
    return schema.execute(query)

Tip: Use this pattern in production for a week to understand your traffic’s complexity distribution before choosing your enforcement threshold.

Real-World Use Cases

E-commerce Platform

Protect your e-commerce API from expensive queries:

from graphql_complexity import ComplexityEstimator, get_complexity
from graphql import build_schema

schema = build_schema("""
    directive @complexity(value: Int!) on FIELD_DEFINITION
    
    type Product {
        id: ID!
        name: String!
        description: String!
        price: Float!
        reviews: [Review!]! @complexity(value: 10)
        relatedProducts: [Product!]! @complexity(value: 15)
    }
    
    type Review {
        id: ID!
        rating: Int!
        comment: String!
        author: User!
    }
    
    type User {
        id: ID!
        name: String!
        orders: [Order!]! @complexity(value: 20)
    }
    
    type Order {
        id: ID!
        items: [Product!]!
        total: Float!
    }
    
    type Query {
        product(id: ID!): Product
        products(limit: Int, offset: Int): [Product!]!
    }
""")

# This query would be too expensive
expensive_query = """
    query ExpensiveQuery {
        products(limit: 100) {
            id
            name
            reviews {
                author {
                    orders {
                        items {
                            relatedProducts {
                                reviews {
                                    comment
                                }
                            }
                        }
                    }
                }
            }
        }
    }
"""

# Reasonable query
reasonable_query = """
    query ReasonableQuery {
        product(id: "123") {
            id
            name
            price
            description
        }
    }
"""

Social Media API

Rate limit complex social graph queries:

from graphql_complexity import ComplexityEstimator, get_complexity
from graphql import build_schema

schema = build_schema("""
    directive @complexity(value: Int!) on FIELD_DEFINITION
    
    type User {
        id: ID!
        username: String!
        followers: [User!]! @complexity(value: 10)
        following: [User!]! @complexity(value: 10)
        posts: [Post!]! @complexity(value: 5)
    }
    
    type Post {
        id: ID!
        content: String!
        author: User!
        likes: [User!]! @complexity(value: 5)
        comments: [Comment!]! @complexity(value: 3)
    }
    
    type Comment {
        id: ID!
        text: String!
        author: User!
    }
    
    type Query {
        user(id: ID!): User
        feed(limit: Int): [Post!]!
    }
""")


class SocialMediaEstimator(ComplexityEstimator):
    """Custom estimator for social media queries"""
    
    def get_field_complexity(self, node, type_info, path) -> int:
        field_name = node.name.value
        
        # Heavy penalty for deeply nested social graphs
        depth = len(path)
        depth_penalty = 2 ** depth if depth > 3 else 1
        
        # Base costs
        costs = {
            'followers': 10,
            'following': 10,
            'posts': 5,
            'likes': 5,
            'comments': 3,
            'feed': 8,
        }
        
        base_cost = costs.get(field_name, 1)
        return base_cost * depth_penalty


# Example: Preventing graph explosion
problematic_query = """
    query DeepSocialGraph {
        user(id: "1") {
            followers {
                following {
                    posts {
                        likes {
                            followers {
                                # This gets exponentially expensive!
                                username
                            }
                        }
                    }
                }
            }
        }
    }
"""

Analytics Dashboard

Manage complexity for data-heavy analytics queries:

from graphql_complexity import ComplexityEstimator, get_complexity
from graphql import build_schema
from datetime import datetime

schema = build_schema("""
    directive @complexity(value: Int!) on FIELD_DEFINITION
    
    type Analytics {
        pageViews: Int! @complexity(value: 5)
        uniqueVisitors: Int! @complexity(value: 5)
        averageSessionDuration: Float! @complexity(value: 10)
        conversionRate: Float! @complexity(value: 15)
        revenueByDay: [DailyRevenue!]! @complexity(value: 20)
    }
    
    type DailyRevenue {
        date: String!
        amount: Float!
        transactions: Int!
    }
    
    type Query {
        analytics(
            startDate: String!
            endDate: String!
            granularity: String
        ): Analytics @complexity(value: 10)
    }
""")


def calculate_analytics_complexity(query: str, date_range_days: int) -> int:
    """Calculate complexity based on date range"""
    base_complexity = get_complexity(
        query=query,
        schema=schema,
        estimator=SimpleEstimator(complexity=1)
    )
    
    # Multiply by date range (more days = more expensive)
    range_multiplier = max(1, date_range_days / 7)  # Weekly baseline
    
    return int(base_complexity * range_multiplier)

Credits

Estimators idea was heavily inspired by graphql-query-complexity.

About

Python library to compute the complexity of a GraphQL operation

Topics

python graphql strawberry graphql-core

License

MIT license

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

graphql_complexity-1.0.0.tar.gz (21.7 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

graphql_complexity-1.0.0-py3-none-any.whl (20.9 kB view details)

Uploaded Python 3

File details

Details for the file graphql_complexity-1.0.0.tar.gz.

File metadata

  • Download URL: graphql_complexity-1.0.0.tar.gz
  • Upload date:
  • Size: 21.7 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.7

File hashes

Hashes for graphql_complexity-1.0.0.tar.gz
Algorithm Hash digest
SHA256 5dfe13ab7c105f24fea16070900b557145e87717f64b92598dcd8035f4212aec
MD5 b592f582ce30a2e3bdf68e3ee55044c9
BLAKE2b-256 45f49d02bc41c62055606de7368863690aab1ded661c9512e14bd40d0e42132c

See more details on using hashes here.

Provenance

The following attestation bundles were made for graphql_complexity-1.0.0.tar.gz:

Publisher: python-publish.yml on Checho3388/graphql-complexity

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file graphql_complexity-1.0.0-py3-none-any.whl.

File metadata

File hashes

Hashes for graphql_complexity-1.0.0-py3-none-any.whl
Algorithm Hash digest
SHA256 cd0c7176cb319b0750bda57147248114d83f2570dfdb7c6db956a6084e70a273
MD5 0635282ca18e9456a1e9ba4eba20ce55
BLAKE2b-256 ba1b58e6666b85a026a4d9ab524250861055ee1f550181b11d0ee37d0b28a1bc

See more details on using hashes here.

Provenance

The following attestation bundles were made for graphql_complexity-1.0.0-py3-none-any.whl:

Publisher: python-publish.yml on Checho3388/graphql-complexity

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page