Cost Analysis

If you expose a GraphQL API to the public internet, you cannot predict what queries clients will send. A single deeply nested query requesting thousands of nodes can bring your server to its knees. Cost analysis prevents this by calculating the cost of a query before executing it and rejecting queries that exceed your budget.

Hot Chocolate implements static cost analysis based on the draft IBM Cost Analysis specification. It assigns weights to fields and estimates list sizes, then computes two metrics: field cost (execution impact) and type cost (data impact). Queries that exceed either limit are rejected before any resolver runs.

Why This Matters for Public APIs#

With REST, each endpoint has a predictable cost. You know that GET /users returns a page of users and takes a roughly constant amount of server time. With GraphQL, a client can construct a query that fans out across relationships:

GraphQL
query {
  users(first: 50) {
    edges {
      node {
        orders(first: 50) {
          edges {
            node {
              items(first: 50) {
                edges {
                  node {
                    product {
                      reviews(first: 50) {
                        edges {
                          node {
                            author {
                              name
                            }
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          }
        }
      }
    }
  }
}

This query requests up to 50 x 50 x 50 x 50 = 6,250,000 nodes. Without cost analysis, the server would attempt to resolve all of them.

Cost analysis catches this at validation time and rejects the query before it consumes resources.

How Cost Is Calculated#

Hot Chocolate assigns default weights and computes two metrics:

  • Field cost represents the execution impact on the server. Async resolvers default to 10, composite types to 1, and scalars to 0.
  • Type cost represents the number of objects the server instantiates.

Field Cost Example#

GraphQL
query {
  book {
    # 10 (async resolver)
    title # 0  (scalar)
    author {
      # 1  (composite type)
      name # 0  (scalar)
    }
  }
}
# Field cost: 11

For paginated fields, costs multiply by the page size:

GraphQL
query {
  books(first: 50) {
    # 10 (async resolver)
    edges {
      # 1  (composite type)
      node {
        # 50 (1 x 50 items)
        title # 0  (scalar)
        author {
          # 50 (1 x 50 items)
          name # 0  (scalar)
        }
      }
    }
  }
}
# Field cost: 111

Type Cost Example#

GraphQL
query {
  # 1 Query
  books(first: 50) {
    # 50 BooksConnections
    edges {
      # 1  BooksEdge
      node {
        # 50 Books
        title
        author {
          # 50 Authors
          name
        }
      }
    }
  }
}
# Type cost: 152

Defaults for Paginated Fields#

Hot Chocolate automatically annotates paginated fields with cost and list size directives. For connection-based pagination:

GraphQL
books(first: Int, after: String, last: Int, before: String): BooksConnection
  @listSize(
    assumedSize: 50
    slicingArguments: ["first", "last"]
    sizedFields: ["edges", "nodes"]
  )
  @cost(weight: "10")

The assumedSize defaults to the MaxPageSize from your pagination options.

Applying a Cost Weight#

Override the default cost for a specific field:

Applying List Size Settings#

For fields that return lists, control how cost analysis estimates the list size:

Inspecting Cost Metrics#

To see the cost of a query without changing enforcement, set the GraphQL-Cost HTTP header:

Header ValueBehavior
reportExecutes the request and includes cost metrics in the response.
validateReturns cost metrics without executing the request.

This is invaluable when tuning your cost configuration. Send representative queries from your client applications and review their costs before deploying changes.

Accessing Costs in Code#

Read cost metrics from IResolverContext or IMiddlewareContext:

C#
public static Book GetBook(IResolverContext context)
{
    var costMetrics = (CostMetrics)context.ContextData[WellKnownContextData.CostMetrics]!;

    double fieldCost = costMetrics.FieldCost;
    double typeCost = costMetrics.TypeCost;

    // Use for logging, monitoring, etc.
}

Tuning Guide#

Start with Defaults#

The defaults (MaxFieldCost = 1000, MaxTypeCost = 1000) work for many schemas. Deploy with defaults first and observe which queries are rejected.

Measure Real Queries#

Use the GraphQL-Cost: report header to measure the cost of your actual client queries. This gives you a baseline to tune from.

Adjust MaxFieldCost and MaxTypeCost#

Increase the limits if legitimate queries are rejected. Decrease them if you want tighter protection. The right values depend on your infrastructure and acceptable load.

C#
builder
    .AddGraphQL()
    .ModifyCostOptions(options =>
    {
        options.MaxFieldCost = 5_000;
        options.MaxTypeCost = 5_000;
    });

Assign Custom Weights to Expensive Fields#

If a resolver calls an external API or runs an expensive query, increase its cost weight:

C#
[Cost(50)]
public static async Task<Report> GetReportAsync(/* ... */)

Use RequirePagingBoundaries#

Force clients to specify first or last on paginated fields. Without this, the cost analyzer uses MaxPageSize as the assumed list size, which may overestimate the cost of well-behaved queries:

C#
builder
    .AddGraphQL()
    .ModifyPagingOptions(opt => opt.RequirePagingBoundaries = true);

Real-World Example#

Consider a product catalog API with this schema:

GraphQL
type Query {
  products(first: Int, after: String): ProductsConnection
}

type Product {
  name: String
  reviews(first: Int, after: String): ReviewsConnection
}

type Review {
  text: String
  author: User
}

With MaxPageSize = 50 and default costs, a query requesting products(first: 50) { ... reviews(first: 50) { ... } } has:

  • Field cost: 10 (products resolver) + 1 (edges) + 50 (node) + 500 (reviews resolver, 10 x 50) + 50 (reviews edges) + 2500 (review node, 50 x 50) + 2500 (author, 50 x 50) = ~5,611
  • Type cost: 1 (Query) + 50 (Products) + 50 (ProductEdges) + 2500 (Reviews) + 2500 (ReviewEdges) + 2500 (Authors) = ~7,601

With default limits of 1,000, this query is rejected. You can either increase the limits or reduce MaxPageSize for the reviews field:

C#
[UsePaging(MaxPageSize = 10)]
public IQueryable<Review> GetReviews([Parent] Product product, CatalogContext db)
    => db.Reviews.Where(r => r.ProductId == product.Id);

Now the cost drops to a level within the default budget.

Options Reference#

Cost Options#

OptionDefaultDescription
MaxFieldCost1_000Maximum allowed field cost.
MaxTypeCost1_000Maximum allowed type cost.
EnforceCostLimitstrueWhether to reject queries that exceed cost limits.
ApplyCostDefaultstrueWhether to apply default cost weights to the schema.
DefaultResolverCost10.0Default cost for an async resolver.
C#
builder
    .AddGraphQL()
    .ModifyCostOptions(options =>
    {
        options.MaxFieldCost = 5_000;
        options.MaxTypeCost = 5_000;
        options.EnforceCostLimits = true;
        options.ApplyCostDefaults = true;
        options.DefaultResolverCost = 10.0;
    });

Filtering Cost Options#

OptionDefaultDescription
DefaultFilterArgumentCost10.0Cost for a filter argument.
DefaultFilterOperationCost10.0Cost for a filter operation.
DefaultExpensiveFilterOperationCost20.0Cost for an expensive filter operation.
VariableMultiplier5Multiplier when a variable is used for the filter argument.
C#
options.Filtering.DefaultFilterArgumentCost = 10.0;
options.Filtering.DefaultFilterOperationCost = 10.0;

Sorting Cost Options#

OptionDefaultDescription
DefaultSortArgumentCost10.0Cost for a sort argument.
DefaultSortOperationCost10.0Cost for a sort operation.
VariableMultiplier5Multiplier when a variable is used for the sort argument.
C#
options.Sorting.DefaultSortArgumentCost = 10.0;
options.Sorting.DefaultSortOperationCost = 10.0;

Disabling Cost Enforcement#

If you protect your API through other means (such as trusted documents), you can disable cost enforcement. The analyzer still computes costs for reporting, but does not reject queries:

C#
builder
    .AddGraphQL()
    .ModifyCostOptions(o => o.EnforceCostLimits = false);

Next Steps#

Edit this page on GitHub
Last updated on by Tobias Tengler