GraphQL Query Complexity Limiting in Magento: Guarding Against Overloaded Queries
AI generated
M2
di.xml
Magento 2 · GraphQL · Security
GraphQL Query Complexity Limiting
how Magento catches overloaded queries and how to extend it yourself

A deeply nested or heavily aliased GraphQL query can trigger a multiple of the database work that a comparable REST request would cause, all within a single HTTP request. This article shows how Magento's built-in queryDepth and queryComplexity limits work, what the alias and query length limits added to the admin in version 2.4.9 additionally cover, and how to add a finer, custom complexity calculation for especially expensive fields.

14 min read queryDepth · queryComplexity Alias and query length limits since 2.4.9

1. Why deeply nested and wide queries are a DoS risk

GraphQL lets a client express in a single request what a REST interface would need dozens of separate calls for. That is exactly what makes GraphQL so attractive for legitimate clients, but it also opens the door to requests that trigger a multiple of the work their size would suggest. Unlike REST, where every endpoint has a roughly bounded cost, a single GraphQL query can trigger a nearly unbounded number of database operations.

A concrete example: a query that walks the category tree several levels deep with children { children { children { ... } } }, or the very same field requested two hundred times under different aliases. Both patterns take relatively few characters to write, yet produce a database load completely out of proportion to the size of the request, which is exactly the classic attack vector against unprotected GraphQL endpoints.


# Example of a pattern that creates disproportionate load without limits
query AliasFlood {
  a1: products(search: "x") { items { sku } }
  a2: products(search: "x") { items { sku } }
  a3: products(search: "x") { items { sku } }
  # ... in practice hundreds more aliases of the same expensive query
}

2. How a GraphQL server calculates depth and complexity in the first place

Magento's GraphQL layer is built on the PHP library webonyx/graphql-php, whose validation rules QueryDepth and QueryComplexity already run against the parsed abstract syntax tree before execution even starts. An overly deep or overly complex query gets rejected before a single resolver has been called, which protects the database far more effectively than any throttling applied after the fact.

QueryDepth simply counts the nesting levels of the requested selections. QueryComplexity assigns one point per requested field by default, nested fields add up into their parent fields, and fragments themselves do not add extra weight. Both rules evaluate only the shape of the query, with no knowledge of what a given field actually costs to resolve.

3. Magento's built-in limits: queryDepth and queryComplexity

Magento wires these two webonyx rules through Magento\Framework\GraphQl\Query\QueryComplexityLimiter in the framework layer, with default values of queryDepth twenty and queryComplexity three hundred. Crucially, both values are plain constructor arguments, configurable only through di.xml, neither value has a store configuration screen in the admin.

The missing admin screen is a deliberate choice, not an oversight: the shape of an allowed query is an architectural decision, not a setting that should be tweaked freely per store view. Any change therefore requires a regular deployment including setup:di:compile.


<!-- app/code/Vendor/GraphQlHardening/etc/di.xml -->
<?xml version="1.0"?>
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:noNamespaceSchemaLocation="urn:magento:framework:ObjectManager/etc/config.xsd">
    <type name="Magento\Framework\GraphQl\Query\QueryComplexityLimiter">
        <arguments>
            <argument name="queryDepth" xsi:type="number">12</argument>
            <argument name="queryComplexity" xsi:type="number">150</argument>
        </arguments>
    </type>
</config>

4. Admin UI limits since 2.4.9: alias and query length limiting

Magento 2.4.9 added a second, genuinely admin configurable protection layer, found under Stores, Configuration, Services, Magento Web API, GraphQl Input Limits. It covers two vectors that pure complexity limiting does not fully close: the number of aliases allowed per query, ten by default, and the maximum character length of a query, just over one megabyte by default.

Behind it sit the config paths graphql/validation/alias_limit_enabled and graphql/validation/maximum_alias_allowed, plus graphql/validation/query_length_limit_enabled and graphql/validation/query_length_limit_allowed. Unlike depth and complexity, these values can be tuned per store without a deployment, useful when a particular headless storefront genuinely needs larger, still legitimate queries.

5. Adjusting depth and complexity values via di.xml

Whether tightening or loosening the defaults makes sense depends on the actual use case. A public, unauthenticated GraphQL endpoint in front of a catalog with deep category trees benefits from stricter limits than the defaults, while an internal PWA Studio frontend running behind persisted queries with a known, bounded query set can comfortably tolerate higher values.

It is important to always pair a loosened complexity limit with persisted queries in production, so the actually executed query shapes stay known and effectively bounded, instead of handing a generous complexity budget to an open endpoint that any client can exploit.


<!-- Example for an internal frontend running behind persisted queries -->
<type name="Magento\Framework\GraphQl\Query\QueryComplexityLimiter">
    <arguments>
        <argument name="queryDepth" xsi:type="number">25</argument>
        <argument name="queryComplexity" xsi:type="number">450</argument>
    </arguments>
</type>

6. Limits of the flat one point model

The default model weighs every field equally, a simple scalar field like sku costs the same single point as a computed field that triggers a full price calculation, a facet aggregation across the whole catalog, or a full text search. A query that looks cheap by field count can still be, by a wide margin, the most expensive query on the entire store.

That mismatch is exactly the blind spot of pure depth and complexity limits, they bound the shape of a query, not its actual resolver cost. An attacker can comfortably stay under three hundred points while still repeatedly hitting the priciest resolvers through carefully placed aliases.

7. Adding a custom complexity calculation for critical fields

Magento exposes neither an @complexity directive nor a per-field cost callback, unlike some other graphql-php based servers. Whoever wants to weight expensive fields higher has to write a custom preference for QueryComplexityLimiter that uses webonyx's fieldComplexity mechanism instead of the flat default, which even lets a field's cost be calculated based on its arguments, such as the requested page size.

This weighting table should stay deliberately small and focused on fields that profiling has actually identified as expensive, rather than trying to weight the entire schema by hand. An exhaustive, manually maintained cost model tends to drift out of sync with a growing schema and becomes a maintenance burden of its own.


<?php
declare(strict_types=1);

namespace Vendor\GraphQlHardening\Model;

use GraphQL\Validator\Rules\QueryComplexity;
use GraphQL\Validator\Rules\QueryDepth;
use GraphQL\Validator\Rules\DisableIntrospection;
use GraphQL\Validator\DocumentValidator;

/**
 * Extends the default complexity rule with a higher weight for known
 * expensive fields, instead of counting every field the same.
 */
class WeightedQueryComplexityLimiter
{
    /**
     * Field name to extra weight, determined by profiling the most expensive resolvers.
     *
     * @var array<string, int>
     */
    private const FIELD_WEIGHTS = [
        'products' => 15,
        'categoryList' => 8,
    ];

    /**
     * Registers the weighted validation rules for the GraphQL request.
     *
     * @param int $maxDepth
     * @param int $maxComplexity
     * @return void
     */
    public function register(int $maxDepth, int $maxComplexity): void
    {
        DocumentValidator::addRule(new QueryDepth($maxDepth));
        DocumentValidator::addRule(new QueryComplexity($maxComplexity, function (int $childrenComplexity, array $args) {
            $fieldName = $args['fieldName'] ?? '';
            $weight = self::FIELD_WEIGHTS[$fieldName] ?? 1;
            $pageSize = (int) ($args['pageSize'] ?? 1);
            return $childrenComplexity + ($weight * max(1, $pageSize));
        }));
        DocumentValidator::addRule(new DisableIntrospection());
    }
}

8. Combining alias and introspection limits: defense in depth

None of these mechanisms is sufficient on its own. Depth and complexity bound the basic shape of a query, the alias limit bounds field multiplication tricks, the query length limit bounds the raw payload size, and disabled introspection in production, also controlled through QueryComplexityLimiter and its associated introspection configuration, removes the ability for attackers to systematically scan the schema for expensive fields in the first place.

It makes sense to treat these mechanisms as complementary layers rather than relying on a single one, a public storefront endpoint benefits from tight depth and complexity values plus the alias and length limits available since 2.4.9 plus disabled introspection, while an authenticated, internally used B2B or PWA Studio endpoint can loosen individual knobs when the traffic is otherwise trusted and monitored.

9. Monitoring and testing against limit violations

Rejected queries raise a GraphQlInputException carrying the respective violation message from QueryDepth or QueryComplexity. These should be logged with enough context, client IP, consumer identifier, and a hash of the query, to distinguish a poorly implemented legitimate client from an actual attack pattern, and to trigger an alert whenever the rejection rate spikes.

Regression tests that extend GraphQlAbstract should cover two things at once: that a legitimately deep catalog query still passes under the configured limit, and that a synthetic, deliberately oversized query gets reliably rejected. That way a future di.xml change immediately stands out if it accidentally loosens the protection.

Mechanism Protects against Configured via Available since
queryDepth (default 20) Overly nested selections di.xml (no admin UI) GraphQL launch, ~2.3.1
queryComplexity (default 300) Too many fields per query di.xml (no admin UI) GraphQL launch, ~2.3.1
Alias limit (default 10) Field multiplication through aliases Stores > Configuration > Services > Magento Web API 2.4.9
Query length limit (~1 MB) Oversized query payloads Stores > Configuration > Services > Magento Web API 2.4.9
Introspection disabled Schema reconnaissance by attackers di.xml (introspection configuration) GraphQL launch

Mironsoft

Magento development, module consulting, and system architecture

A Magento project that needs a second opinion or experienced execution?

We build custom Magento modules, advise on architecture decisions, and take on complex implementations, from service contract planning to production-ready deployment.

Architecture Consulting

Have module and system architecture thought through properly before you build.

Custom Module Development

Build custom Magento modules cleanly, following best practices.

Code Review & Audit

Have existing modules reviewed for performance, security, and maintainability.

10. Summary

Query Complexity Limiting in Magento: The Essentials

Default values

queryDepth 20 and queryComplexity 300 are active out of the box, but configurable only through di.xml.

New since 2.4.9

Alias limit and query length limit can be configured directly in the admin for the first time.

Blind spot

The default model counts every field the same, expensive resolvers need their own weighting.

Recommendation

Combine several mechanisms instead of relying on a single limit.

11. FAQ: Query Complexity Limiting in Magento: The Essentials

1Where do I configure queryDepth and queryComplexity in Magento?
Only through di.xml, as constructor arguments of Magento\Framework\GraphQl\Query\QueryComplexityLimiter, there is no admin screen for it.
2What counts as one complexity point?
One point per requested field by default, nested fields add up into their parent fields, fragments themselves do not add extra weight.
3Since when have alias and query length limits existed in the admin?
Since Magento 2.4.9, configurable under Stores, Configuration, Services, Magento Web API, GraphQl Input Limits.
4Can I assign a higher complexity to individual fields?
Not out of the box, that requires a custom preference for QueryComplexityLimiter with a fieldComplexity mapping following webonyx's convention.
5How high should queryComplexity be for a public storefront endpoint?
There is no universal value, a sensible target sits just above what real production storefront queries actually need, determined through logging before switching over.
6Does a low complexity limit also protect against expensive individual fields?
Only indirectly, the default model weighs every field the same, an expensive field like a facet aggregation counts exactly as much as a simple scalar field.
7Should I disable introspection in production?
Recommended for public endpoints, since a fully inspectable schema makes it considerably easier for attackers to locate expensive fields.
8How do I combine these limits sensibly?
Depth and complexity for the basic query shape, alias and length limits against multiplication tricks and oversized payloads, introspection disabled against reconnaissance, all as complementary layers.
9How do I tell whether a limit is set too tight?
Through logging of rejected queries with their error message, a rise in legitimate rejections after a deploy usually points to a limit that is too tight rather than an actual attack.
10Do I still need custom complexity rules if I use persisted queries?
Persisted queries reduce the risk substantially since only known query shapes get executed, but they do not replace limits as long as registering clients are allowed to add new queries themselves.