Building GraphQL Performance Monitoring in Magento 2
AI generated
M2
di.xml
Magento 2 · GraphQL · Performance Monitoring
Building GraphQL Performance Monitoring in Magento 2
making resolver times, query complexity, and alerting visible

Without dedicated GraphQL performance monitoring, every nested storefront query remains a blackbox: which resolver is slow, which query grows too complex, and which trend builds up over weeks can only be guessed at without measurement. Resolver tracing, query complexity limits, and a connected APM system make exactly these questions answerable.

18 min read Resolver Tracing · Query Complexity · APM · Alerting Magento 2.4.x · GraphQL

1. Why GraphQL monitoring is different from REST monitoring

Classic REST monitoring is oriented around endpoints: GET /products has a response time, a status code, and a fixed set of possible queries. GraphQL, by contrast, has only a single endpoint, /graphql, behind which countless different query shapes hide. A GraphQL performance monitoring setup that only measures the endpoint's overall response time loses exactly the information that is decisive for troubleshooting: which field, which resolver, and which nesting depth was responsible for a slow response.

This peculiarity requires a different monitoring approach: instead of endpoint metrics, GraphQL performance monitoring needs field and resolver level metrics. Two requests to the same endpoint can have completely different runtimes, depending on which fields were requested and how deeply the query is nested. Without this granularity, a performance problem can be recognized as a symptom, but cannot be traced back to the causing line of code.

A second difference concerns the attack surface for performance problems itself: a maliciously or accidentally too deeply nested query can trigger exponentially many database calls, a behavior that practically never occurs in REST APIs with a fixed structure. GraphQL performance monitoring therefore must not only reactively measure response times, but proactively estimate the complexity of a query before it even executes.

2. Which metrics matter for Magento GraphQL

For meaningful GraphQL performance monitoring in Magento, at least four metric categories are relevant. First, resolver runtime per field, measured from the beginning to the end of the resolve() method, to identify slow individual resolvers. Second, query complexity, a numeric value combining field count and nesting depth that estimates how potentially expensive a query is, independent of its actual runtime. Third, the number of triggered database queries per request, to uncover N+1 patterns before they turn into real load problems.

The fourth category concerns error rates per field and category, as described in the chapter on GraphQL error handling, because a sudden increase in graphql-no-such-entity errors on a specific field is often a precursor to a bigger data problem. Together, these four metric categories give a more complete picture than a single overall response time metric, which frequently obscures the real causes.

3. Measuring resolver times with a custom plugin

Since Magento does not ship built in field level timing for GraphQL, it is worth adding a generic plugin on Magento\Framework\GraphQl\Query\Resolver\ResolverInterface that measures the runtime of every resolver call and logs it together with the field name and query path. This plugin works regardless of whether the respective resolver belongs to the Magento core or to a custom GraphQL mutation, as long as it implements the standard interface.


<?php
declare(strict_types=1);

namespace Mironsoft\GraphQlMonitoring\Plugin;

use Magento\Framework\GraphQl\Config\Element\Field;
use Magento\Framework\GraphQl\Query\Resolver\ContextInterface;
use Magento\Framework\GraphQl\Query\ResolverInterface;
use Magento\Framework\GraphQl\Schema\Type\ResolveInfo;
use Psr\Log\LoggerInterface;

/**
 * Measures and logs the execution time of every GraphQL resolver call.
 */
final class MeasureResolverTime
{
    private const SLOW_THRESHOLD_MS = 100;

    /**
     * @param LoggerInterface $metricsLogger Dedicated logger channel for GraphQL performance metrics
     */
    public function __construct(
        private readonly LoggerInterface $metricsLogger
    ) {
    }

    /**
     * Wrap the resolve call with timing instrumentation.
     *
     * @param ResolverInterface $subject
     * @param \Closure $proceed
     * @param Field $field
     * @param ContextInterface $context
     * @param ResolveInfo $info
     * @param array|null $value
     * @param array|null $args
     * @return mixed
     */
    public function aroundResolve(
        ResolverInterface $subject,
        \Closure $proceed,
        Field $field,
        $context,
        ResolveInfo $info,
        array $value = null,
        array $args = null
    ) {
        $startTime = microtime(true);
        $result = $proceed($field, $context, $info, $value, $args);
        $durationMs = (microtime(true) - $startTime) * 1000;

        if ($durationMs > self::SLOW_THRESHOLD_MS) {
            $this->metricsLogger->warning('Slow GraphQL resolver', [
                'field' => $field->getName(),
                'resolver' => get_class($subject),
                'duration_ms' => round($durationMs, 2),
            ]);
        }

        return $result;
    }
}

Important for production use: logging should only trigger above a threshold, as shown in the example with SLOW_THRESHOLD_MS, instead of logging every single resolver execution. Otherwise the monitoring itself creates a significant additional load through excessive logging, an effect that runs counter to the actual goal of performance optimization.

4. Checking query complexity and depth before execution

Besides reactive timing, proactive query analysis is part of a complete GraphQL performance monitoring setup. A query complexity check calculates a cost value before the actual execution, usually based on the number of requested fields weighted by their nesting depth, and rejects queries above a configured limit directly, before even a single resolver is called. Magento offers the configuration graphql/query_complexity_limit or comparable settings in the admin area under GraphQL settings for this.

A complementary depth check additionally limits the maximum nesting depth independent of field count, because even a few but very deeply nested fields, for example recursive category trees, can lead to exponential effort. Both checks together form a first line of defense against accidentally or maliciously too expensive queries, long before a single database query is executed, and thereby complement the whitelisting described in the article on persisted queries with an additional, independent protection layer.

5. Integrating with APM tools

For teams already running an application performance monitoring tool such as New Relic, Blackfire, or an OpenTelemetry based setup, it is worth integrating GraphQL spans into the existing trace infrastructure, instead of building a separate, isolated GraphQL dashboard. Every resolver call is captured as its own span within the parent request trace, with field name, duration, and relevant tags such as query complexity.

This integration makes it possible to trace a slow GraphQL request all the way down to the underlying database query or external API call, within the same trace in which classic storefront requests also appear. For Magento projects with OpenTelemetry support, the resolver plugin shown above can be extended with span creation, so that every measured resolver call simultaneously shows up as a visible span in the APM tool instead of ending up only in a separate log file.


<?php
declare(strict_types=1);

namespace Mironsoft\GraphQlMonitoring\Plugin;

use OpenTelemetry\API\Trace\SpanKind;
use OpenTelemetry\API\Trace\TracerInterface;

/**
 * Creates an OpenTelemetry span for every GraphQL resolver execution.
 */
final class TraceResolverSpan
{
    /**
     * @param TracerInterface $tracer Configured OpenTelemetry tracer instance
     */
    public function __construct(
        private readonly TracerInterface $tracer
    ) {
    }

    /**
     * Wrap the resolve call in an OpenTelemetry span.
     *
     * @param object $subject
     * @param \Closure $proceed
     * @param mixed ...$args
     * @return mixed
     */
    public function aroundResolve(object $subject, \Closure $proceed, ...$args)
    {
        $span = $this->tracer->spanBuilder('graphql.resolve.' . get_class($subject))
            ->setSpanKind(SpanKind::KIND_INTERNAL)
            ->startSpan();

        try {
            return $proceed(...$args);
        } finally {
            $span->end();
        }
    }
}

6. Dashboards and alerting thresholds

Raw data alone is not monitoring. A dashboard for GraphQL performance monitoring should combine at least three views: the slowest resolvers of the last 24 hours sorted by average duration, the distribution of query complexity values across all requests, and the error rate per category over time. Together, these three views reveal both acute outliers and slow, creeping degradations, which are easily overlooked when only looking at a plain average.

Alerting thresholds should not be based on absolute values but on relative deviations from the historical normal state, because different fields naturally have different baseline runtimes. A field that normally takes 20 milliseconds and suddenly needs 200 milliseconds represents a tenfold increase and is worth alerting on, even if 200 milliseconds looks acceptable in absolute terms. A static threshold of, say, 500 milliseconds for all fields would completely miss this relative but significant increase.

7. Building a slow query log for GraphQL

Analogous to the well known MySQL slow query log, it is worth building a dedicated, structured log for GraphQL requests that exceed a defined total runtime. Unlike the pure resolver timing from section three, this log captures the complete request including the original query, the variables, and the total response time, so that slow requests can also be reproduced exactly after the fact.

Such a slow query log for GraphQL is typically implemented as a custom plugin on the GraphQL front controller, similar to the approach in the article on persisted queries, measures the total duration from request start to response delivery, and writes a structured log entry including the complete query when the threshold is exceeded. For subsequent analysis, a simple script that extracts the most common slow query patterns from the log and sorts them by frequency is suitable for concentrating optimization effort on the most relevant cases.

8. Embedding monitoring into the development process

Pure production monitoring often comes too late: a performance problem that only becomes noticeable after deployment has already affected real users. More effective GraphQL performance monitoring therefore integrates query complexity checks and resolver timing into the CI pipeline early on, for example through automated tests that execute known critical queries against a test database and block the merge request on significant regressions.

A simple but effective approach: a set of representative queries, for example the product page query with typical nesting depth, is executed automatically on every pull request, the measured response time is compared against the value from the last stable release, and a warning is left in the pull request comment if there is a degradation beyond a defined percentage. This regression test catches performance problems before they even reach production, instead of discovering them only in live operation through monitoring alerts.

9. Monitoring approaches compared

The following table compares the presented monitoring approaches based on their respective focus.

Approach Focus When used
Resolver timing plugin Runtime per field and resolver Continuously in production
Query complexity limit Cost estimation before execution On every incoming request
APM integration End to end trace across system boundaries For complex, distributed setups
Slow query log Full reproducibility of slow requests For subsequent detailed analysis
CI regression test Preventing new performance regressions Before every deployment

None of these approaches fully replaces the others. A mature GraphQL performance monitoring setup typically combines at least resolver timing and query complexity limits as a foundation, supplemented with APM integration and CI regression tests as the project grows.

Mironsoft

Magento 2 GraphQL performance and monitoring

Flying blind on GraphQL performance in your shop?

We build a complete GraphQL performance monitoring setup for your Magento storefront API: resolver tracing, query complexity limits, APM integration, and CI regression tests.

Resolver tracing

Field level timing and integration into existing APM tools

Complexity limits

Limiting query cost before execution and blocking outliers

CI regression

Automated detection of performance regressions before deployment

10. Summary

Solid GraphQL performance monitoring in Magento 2 needs more than a single endpoint response time. Resolver level timing shows which field is actually slow, query complexity and depth limits prevent overly expensive queries before execution, and integration into an existing APM system makes GraphQL traces part of the same observability as the rest of the application. A slow query log complements these building blocks with full reproducibility of slow individual cases.

The biggest lever lies in not building monitoring reactively only after a production incident, but proactively integrating it into the development process: regression tests in the CI pipeline catch performance degradations before they even reach users. Together, these building blocks form a system that makes GraphQL performance problems in Magento visible, instead of discovering them only through customer complaints.

GraphQL Performance Monitoring in Magento 2 — Key Takeaways

Resolver timing

A plugin on ResolverInterface measures runtime per field, with a threshold against logging overhead.

Query complexity

Proactive cost estimation before execution prevents overly expensive queries early.

APM & slow query log

End to end traces and full reproducibility of slow requests.

CI integration

Regression tests catch performance problems before deployment, not only afterwards.

11. FAQ: GraphQL Performance Monitoring in Magento 2

1Why isn't endpoint time enough?
GraphQL has only one endpoint with countless query shapes, overall time loses field level detail.
2Which metrics matter?
Resolver runtime, query complexity, DB call count, and error rates per category.
3How do you measure resolver times?
Through a generic plugin on ResolverInterface with threshold based logging.
4What is query complexity?
A cost value from field count and nesting depth to estimate expensive queries before execution.
5Why not log every call?
Excessive logging itself creates additional load, a threshold avoids that counterproductive effect.
6How to integrate with APM tools?
By adding span creation to the resolver plugin, for example with OpenTelemetry, in the same request trace.
7Why relative alerting thresholds?
Because fields have different baseline times, a relative deviation is more meaningful.
8What does a slow query log capture?
The full request including query and variables for exact reproducibility.
9How to embed it in CI?
Automated comparison tests against the last stable release with a regression warning.
10Does one approach replace all others?
No, the approaches complement each other in a complete monitoring setup.