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.
Table of Contents
- 1. Why GraphQL monitoring is different from REST monitoring
- 2. Which metrics matter for Magento GraphQL
- 3. Measuring resolver times with a custom plugin
- 4. Checking query complexity and depth before execution
- 5. Integrating with APM tools
- 6. Dashboards and alerting thresholds
- 7. Building a slow query log for GraphQL
- 8. Embedding monitoring into the development process
- 9. Monitoring approaches compared
- 10. Summary
- 11. FAQ
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.