DevOps

What we monitor differently on a GraphQL API versus REST

OPS

Our standard monitoring setup for REST APIs is built around per-endpoint metrics — request count, latency, and error rate broken down by route. A GraphQL API breaks that immediately, since every request hits the same single `/graphql` endpoint regardless of what it actually asks for, which means a monitoring dashboard built around REST-era assumptions shows a single, largely uninformative aggregate metric where it used to show a useful breakdown by route.

Why per-endpoint thinking does not translate directly

The whole premise of REST-style per-endpoint monitoring is that a URL path is a reasonable proxy for "what kind of work is this request doing," which is a fair assumption when each endpoint does one relatively fixed thing. GraphQL's single endpoint accepts an arbitrary query shape on every request, so the URL path tells you nothing useful about whether that particular request touched one lightweight field or fanned out into a dozen expensive nested resolvers — you have to look inside the request itself to know anything meaningful about its actual cost.

What we track instead

We now track metrics at the level of individual resolvers instead, since that is where the real latency and error variance lives — a query fetching a user's basic profile and a query fetching their full order history with nested line items both hit the same endpoint but have wildly different costs, and only resolver-level metrics actually distinguish between them. Every resolver reports its own execution time and error rate independently, tagged with the specific type and field it resolves, which lets us build a dashboard that actually resembles the useful per-endpoint breakdown we lost when we moved off REST.

Query complexity as its own tracked metric

We also track query complexity as its own metric, since a single deeply nested query from a misbehaving client can do more backend work than hundreds of simple ones, and endpoint-level request counts alone would never surface that. A client library with a bug causing it to request several levels deeper than intended, for instance, would show up as a completely unremarkable single request in a naive per-endpoint view, while our complexity-aware monitoring flags it immediately as an outlier worth investigating regardless of how it looks from a pure request-count perspective.

Specific dashboards we ended up building

  • A resolver-level latency heatmap, sorted by total time contributed across all requests in a given window, which reliably surfaces the actual biggest opportunities for optimization rather than relying on guesswork about which part of the schema is slow
  • A query complexity distribution chart, letting us spot a sudden shift toward more complex queries before it becomes an actual performance incident, since a gradual creep in average query complexity is exactly the kind of slow-moving problem that a simple latency alert alone would catch too late
  • A per-client breakdown, since our API serves both our own web frontend and a couple of third-party integration partners, and being able to attribute load to a specific consumer has been essential for a couple of conversations where a partner's queries turned out to be considerably more expensive than intended on their end

An incident this approach caught early

A few weeks after this monitoring went live, the complexity distribution chart flagged a gradual upward trend that traced back to a recently shipped mobile app update requesting a newly added, deeply nested field on nearly every screen load, a change the mobile team had not realized carried this much backend cost when they added it. Catching this from a complexity trend line, days before it became a genuine latency problem visible to users, is exactly the kind of early warning a REST-style monitoring setup built around simple request counts would never have surfaced in time.

What this took to set up, honestly

It took more setup than our REST monitoring did, since resolver-level instrumentation means wiring timing and error-reporting code into every individual resolver function rather than a single piece of middleware wrapping an entire route the way REST monitoring typically works. But without resolver-level visibility we would have been flying blind on where a GraphQL API's actual performance problems live, and the upfront investment has already paid for itself in the specific incident described above, caught early enough to fix calmly rather than during a live production slowdown.

Alerting thresholds we settled on after some trial and error

Our first attempt at alerting thresholds was too sensitive, generating enough noisy, low-value alerts in the first couple of weeks that the team started quietly ignoring them, which is exactly the failure mode any alerting system needs to avoid at all costs. We recalibrated based on actual historical resolver latency distributions rather than round numbers that felt intuitively reasonable, and settled on alerting only when a resolver's latency crosses a threshold derived from its own historical baseline rather than a single fixed number applied uniformly across every resolver regardless of how naturally expensive that particular field's work is.

How this monitoring approach differs for mutations versus queries

Mutations carry a different risk profile than queries, since a slow or failing mutation often means a user-facing action — placing an order, updating a profile — did not complete successfully, which is a meaningfully worse outcome than a slow read. We set tighter alerting thresholds and separate dashboards specifically for mutation resolvers, treating any mutation failure rate above a very low threshold as a more urgent signal than the equivalent failure rate on a read-only query resolver, mirroring the same reasoning we apply to distinguishing checkout-critical work from lower-stakes background work elsewhere in our infrastructure.

Sharing this visibility with the mobile and frontend teams directly

Rather than keeping this monitoring purely as a backend team concern, we gave the mobile and frontend teams direct, read-only access to the resolver-level dashboards, since a frontend team member noticing their own newly shipped feature is driving an unusual amount of resolver load is valuable information they are well positioned to act on quickly, often faster than a backend team member who was not involved in that specific frontend change and would need more context before recognizing the connection.

← Back to the journal

Have a project in mind?
Let’s talk.

Tell us where you are and where you want to go. We'll map the fastest route between the two.

Currently accepting new clients