← Recipes

阅读中文版本

Track GraphQL Operation Usage to Find Abusive Queries

Fingerprint every GraphQL operation by name and hash, aggregate call counts and depth, and spot the one client sending giant or unbounded queries.

~35 minapigraphqlobservability

Task

Instrument a GraphQL server to fingerprint operations and rank them by frequency, depth, and cost to find abusive traffic.

When to use this

One integration partner keeps sending a deeply nested query that costs more than every other client combined, but it has no name and is invisible in normal request logs.

Prerequisites

  • A GraphQL server with a plugin or middleware hook, such as Apollo plugins
  • Structured log output with a shared query field
  • A log aggregation tool or plain grep

Steps

1. Add a plugin that fingerprints each operation.

const usagePlugin = {
  requestDidStart() {
    return {
      willSendResponse(ctx) {
        const name = ctx.operationName ?? "anonymous";
        const hash = crypto.createHash("sha256")
          .update(ctx.request.query ?? "").digest("hex").slice(0, 12);
        log.info({ name, hash, depth: depthOf(ctx.request.query) }, "op");
      },
    };
  },
};

Expected output: Every request logs a name, a stable hash of the query text, and its nesting depth, turning anonymous traffic into ranked fingerprints.

2. Rank operations by call volume.

grep '"op"' access.log | jq -r '.name + " " + .hash' | sort | uniq -c | sort -rn | head -15

Expected output: The top entries show the hottest operations; an anonymous hash near the top is your unnamed heavy hitter.

3. Rank by depth and estimated cost.

grep '"op"' access.log | jq -r '.hash + " " + (.depth|tostring)' | sort -k2 -rn | head -10

Expected output: A depth of 12 on a list field stands out immediately and explains the outlier cost per request.

4. Introduce named and persisted queries for third parties.

const server = new ApolloServer({
  validationRules: [depthLimit(8)],
  plugins: [usagePlugin],
});

Expected output: Anonymous queries beyond depth 8 are rejected with a clear error, and the partner must register a named, reviewed operation.

Troubleshooting

Problem

Anonymous queries dominate traffic

Solution

Require operation names in CI for first-party clients and enable persisted queries so third parties send a hash instead of arbitrary query text.

Problem

Depth is high but cost is low

Solution

Depth alone overestimates cost when branches return scalars; combine depth with field-level cost weights before rejecting anything.

Problem

Hash changes for the same logical query

Solution

Whitespace and comments change the hash; normalize with a printer such as graphql print before hashing, or rely on the operation name plus variables signature.

Limitations

  • Fingerprinting sees query text, not execution cost; a cheap-looking query can still be expensive with large variable values.
  • Persisted queries need registry tooling and add a deployment step for clients.

Anonymous GraphQL is unrankable GraphQL

A REST access log names endpoints, so the noisy client is one sort away. GraphQL without operation names gives you a wall of POST /graphql and no ranking at all. Fingerprinting fixes this: log the operation name and a hash of the normalized query text for every request, and within a day you have a leaderboard of who actually hits the API and how expensive each shape is.

# Rank operations by volume
grep '"op"' access.log | jq -r '.name + " " + .hash' \
  | sort | uniq -c | sort -rn | head -15

Depth is the cheapest cost signal

Full query cost analysis needs per-field weights and variable inspection, but nesting depth is free to compute and catches the common abuse: unbounded lists nested into lists. Rejecting depth above a reviewed ceiling stops the worst shapes while the leaderboard tells you exactly whom the new rule affects.

Turn fingerprints into contracts

Once operations are named and ranked, promote the important ones to persisted queries: clients send a hash, the server executes the registered text. That removes arbitrary query text from the wire entirely, shrinks request payloads, and makes the usage leaderboard authoritative instead of approximate.