Frequently asked questions

GraphQL caching

What is a GraphQL CDN?
A GraphQL CDN is a cache that sits in front of a GraphQL API and serves read responses from locations close to your users. A regular, URL-based CDN often can't derive a useful cache key from the URL alone, because GraphQL sends almost everything to one endpoint over POST (queries can use GET, but most clients don't), so a GraphQL CDN parses the query to decide what to cache and how to invalidate it. Why you can't just put a CDN in front of GraphQL →
Can you just put a normal CDN in front of GraphQL?
Not effectively. A stock CDN keys cache entries on HTTP method and URL, but GraphQL sends almost everything as POST to a single endpoint, so to a URL-based CDN the requests look alike at the HTTP layer. Without looking inside the query body, such a CDN can't reliably tell two requests apart or safely cache them. Why you can't just put a CDN in front of GraphQL →
Why is GraphQL cache invalidation so hard?
In REST a write maps cleanly to a URL you can purge. In GraphQL there is no URL, and a single mutation can stale fragments of many unrelated cached responses. The standard fix is surrogate keys: tag each cached response with the entities it contains (like User:1) and purge by tag whenever a mutation touches them. Why you can't just put a CDN in front of GraphQL →
What should a good GraphQL CDN provide?
Schema-driven cache configuration, entity-level surrogate keys with a real purge API, explicit auth scoping so personalized data is never cached by accident, and observability for hit rate, staleness, and purge lag. Why you can't just put a CDN in front of GraphQL →
How do I cache a GraphQL API when every request is a POST?
You cache by query content, not by URL. Because GraphQL usually sends every request as a POST to one endpoint, an ordinary URL based cache sees them all as the same uncacheable call. A GraphQL-aware cache instead parses the request and builds a key from the operation, its variables, and the relevant context, so identical reads share a cached result while different ones stay separate. That moves caching from the HTTP envelope, where GraphQL gives it nothing to work with, to the query itself. GraphPilot is a GraphQL-aware cache that does this for you at the edge, keying on the operation and variables so identical reads are served close to your users without a single line of caching code in your API.
How do I keep cached GraphQL data from going stale?
Tie invalidation to the data, not just to a clock. Time based expiry alone forces a trade between serving stale data and caching almost nothing. The more precise approach is to track which types or entities a cached response depends on, then invalidate exactly those entries when the underlying data changes, for example after a mutation. That lets you cache aggressively while still dropping the specific results that a write has made stale, instead of clearing everything or waiting for a timer. GraphPilot tracks these dependencies for you and invalidates the affected entries on writes, so its edge cache stays aggressive without serving data a mutation has already changed.
How do I tell whether caching is actually helping my GraphQL API?
Measure the cache hit ratio and the latency difference between hits and misses. A high hit ratio with meaningfully faster hits tells you the cache is absorbing real traffic; a low ratio usually means your cache keys are too specific, your data changes too often, or queries vary in ways that prevent reuse. Watch origin load as well, since the point of caching is to keep repeat reads off your servers. Without these numbers you are guessing, so make hit ratio and origin offload first-class metrics. GraphPilot reports the cache status of every request, and its Insights show the cache hit share and latency per request and per operation, so you can see how much traffic the edge cache is actually absorbing rather than estimating it.
What should I monitor on a GraphQL API in production?
Because one endpoint hides many operations, monitor per operation rather than only at the endpoint level. Track latency and error rate by operation name, resolver level timings to find slow fields and N+1 patterns, query complexity and rejected queries to see abuse and tune limits, and cache hit ratio and origin load if you cache. Schema usage over time also shows which fields are safe to deprecate. Aggregate endpoint metrics will tell you something is wrong but rarely which query is responsible. GraphPilot's Insights break traffic down by operation, with latency, error rate and cache hits for each, so you can see which query is responsible without instrumenting every resolver yourself. Resolver-level timings still come from your own tracing.
Can I add edge caching without changing my API?
Usually yes, by putting a GraphQL-aware cache in front of your existing API rather than building caching into it. GraphPilot is an edge proxy that sits ahead of your GraphQL API, parses incoming queries, and caches results close to your users worldwide without changes to your schema or resolvers. Reads that can be served from the edge never reach your origin, and writes can invalidate the affected entries so clients still see fresh data. The caching lives at the boundary, so your API stays as it is.

GraphQL federation

How do I adopt GraphQL federation without a big-bang rewrite?
Adopt it incrementally. Keep your current GraphQL service running as the first subgraph, put a gateway or composition layer in front of it, and then carve out new domains as additional subgraphs over time. Each team owns its own subgraph and schema, and the gateway composes them into one supergraph that clients query as a single API. Because clients keep talking to one endpoint, you can move ownership of types and fields gradually without a coordinated rewrite or a flag day. Since GraphPilot sits in front of your gateway as an edge proxy, the caching and invalidation you rely on apply across the whole supergraph as you add subgraphs, and so do its edge controls (token verification, introspection blocking, depth and size limits, and an operation allowlist).
What is the difference between a federated gateway and schema stitching?
Both combine multiple GraphQL services into one schema, but they differ in where the knowledge lives. Schema stitching merges schemas at the gateway, so the gateway holds the rules for how types relate, which becomes hard to maintain as services grow. Federation pushes that ownership down: each subgraph declares how its types extend and reference types from others, and the gateway composes the result from those declarations. Federation generally scales better across many teams because the relationships are defined and versioned alongside the services that own them. Whichever you run, GraphPilot can sit in front of the composed graph and add edge caching and edge security controls so each subgraph does not have to reimplement them.
How do I stop one subgraph change from breaking the whole supergraph?
Validate composition before anything ships. Run schema composition in continuous integration so a subgraph change that would break the combined schema fails the build rather than production. Add composition checks against the currently deployed supergraph to catch breaking changes, and use a schema registry as the single source of truth for what is live. Deprecating a field, rather than removing it, gives clients time to migrate. The principle is to treat the supergraph schema as a contract that every subgraph change is checked against. GraphPilot is schema-driven as well, so the caching policy it applies follows your schema as it evolves instead of being configured by hand for every change.
Do I even need federation if I only have one GraphQL service?
Usually not. Federation solves an organizational problem: many teams owning different parts of one graph without stepping on each other. If you have a single service and a single team, it mostly adds operational overhead you do not need yet. Keep your schema modular and your boundaries clean so you can split later, but reach for federation when you actually have multiple services or teams that need independent ownership, not before. When you do split, GraphPilot keeps your edge caching working across the supergraph, so growing into federation later does not mean rebuilding your edge layer.
How does GraphPilot help with federation?
GraphPilot sits in front of your GraphQL setup as an edge proxy and applies its caching and invalidation across a federated graph the same way it does for a single service, so you do not lose that layer when you move to a supergraph. The same goes for its edge controls: JWT verification, blocked introspection, depth and size limits and an allowlist of registered operations apply in front of the supergraph as a whole. The idea is that scaling out to multiple subgraphs should not mean rebuilding your edge layer for each one.

GraphQL performance

How do I make my GraphQL API faster?
Start by finding where the time actually goes: a single GraphQL request can fan out into dozens of resolver calls, so per-resolver tracing usually reveals more than aggregate endpoint latency. The biggest wins are normally batching database access to remove N+1 queries, caching responses or partial results for fields that do not change on every request, and trimming over-large payloads. Schema design matters too, since expensive fields behind list types multiply quickly. The quickest win is often to put GraphPilot in front of your API: as a GraphQL-native edge proxy it adds caching and invalidation at the boundary and serves repeat queries close to your users worldwide, so your servers spend their time only on the queries that truly need fresh data.
Why is my GraphQL API slow even when my database is fast?
Usually because of how the work is shaped, not how fast each query is. The classic cause is the N+1 problem: resolving a list of items triggers one extra query per item, so a fast database still runs hundreds of round trips for one request. Resolver waterfalls add to this when fields depend on each other and run in sequence instead of in parallel. A DataLoader-style batching layer collapses those per-item queries into one, and parallelizing independent resolvers removes avoidable waiting. The fix is in the access pattern, not in the database engine. Even after you remove the N+1 problem, the same reads still travel to your origin on every request, so putting GraphPilot in front lets you serve those repeat reads from cache at the edge and keeps a healthy database from re-answering identical queries.
How do I stop clients from over-fetching and bloating responses?
GraphQL lets clients request exactly the fields they need, but it also lets them ask for large nested lists in a single query. Guard against this with pagination on every list field, sensible default and maximum page sizes, and depth or complexity limits that reject queries which would return too much. Persisted queries help on trusted clients by allowing only known, reviewed operations. The goal is to make the cheap path the default and require deliberate effort to ask for a lot of data. GraphPilot enforces part of this at the edge: a query depth limit that is on by default, caps on document size and on the number of selections, and an optional allowlist of registered operations. It does not price query cost, so keep pagination and page-size limits in your API, and let edge caching keep repeat reads from reaching your origin at all.
Can I add response caching to GraphQL the way I do with REST?
Not directly, because most GraphQL traffic is a POST to a single endpoint with the real query in the body, so the URL based caching that REST relies on cannot tell two requests apart. To cache GraphQL you need a layer that parses the query, builds a cache key from the operation and its variables, and knows which results are safe to reuse. Once that is in place you can cache full responses or individual types, and invalidate them when the underlying data changes. The mechanism is different from REST, but the payoff is the same: fewer repeat trips to your origin. GraphPilot provides exactly this GraphQL-aware caching as an edge proxy in front of your API, so you get REST-like cache benefits without building the keying and invalidation yourself.
Can I speed up GraphQL without rewriting my resolvers?
Often, yes. A lot of latency comes from repeatedly answering the same reads, which you can remove without touching resolver code by caching responses in front of your API. GraphPilot is a GraphQL-native edge proxy that sits ahead of your existing API and adds caching and invalidation at the boundary, so repeat queries are served close to your users worldwide and never reach your origin. You still fix genuine N+1 and schema issues in your resolvers, but you can recover a large share of the latency before changing a line of server code.

GraphQL security

Why isn't resolver-level GraphQL security enough?
For cost and abuse controls, resolver checks run after the request has already reached your origin and paid the cost of parsing, planning, and partial execution, so a query designed to overload you may have done much of its damage by the time a resolver rejects it. Resolver logic also tends to live scattered across services and fields, so the same rule gets reimplemented inconsistently. The boundary you can defend first is in front of the origin, where a query is inspected and rejected before execution starts. Deep business authorization still belongs in your application. Resolver-level GraphQL security isn't enough on its own →
What are the most important GraphQL attacks to defend against?
Complexity and depth denial-of-service (deeply nested or cyclic queries that explode into huge amounts of work), aliasing and batching abuse (many operations smuggled into one request to bypass rate limits), introspection leakage (using the schema to map your attack surface), and broken field-level authorization (fields that return data the viewer should not see). The query body, not the HTTP envelope, is what exposes them: the first three are decided entirely by the query, and broken field-level authorization depends on identity, tenant, and business rules, but the query body still reveals which fields and shapes need a policy decision. Resolver-level GraphQL security isn't enough on its own →
Can a WAF or normal CDN protect a GraphQL API?
Only if they can inspect the GraphQL body. A WAF or CDN keys on method, URL, and headers, but GraphQL sends almost everything as POST to a single endpoint with the real intent in the body. The envelope looks the same for a harmless query and a malicious one, so envelope-only tools that don't parse the query have limited visibility into GraphQL-specific attacks; a layer that does parse it can apply meaningful controls. Resolver-level GraphQL security isn't enough on its own →
What is query cost analysis in GraphQL?
Query cost analysis estimates how expensive a query will be before it runs, by walking the parsed query and assigning weights to fields, nesting, and list multipliers. The estimate lets you reject a query that exceeds a budget, and lets you rate limit by cost rather than by request count, so one heavy request counts for more than many cheap ones. Resolver-level GraphQL security isn't enough on its own →
How do I make my GraphQL API more secure?
Defend the query before it runs, not only inside your resolvers. The core controls are limiting query depth and complexity so a single request cannot fan out into an enormous amount of work, rate limiting by the cost of a query rather than by request count, enforcing authentication and field-level authorization, and being deliberate about what introspection exposes. Doing these at the boundary, in front of your origin, means an abusive query is rejected before it consumes resources, and the same rules apply consistently instead of being reimplemented in every resolver. GraphPilot applies several of these at the edge today: it verifies JWTs before a request reaches your origin, blocks introspection by default, limits query depth (10 levels unless you change it), caps request body, document size and selections, can restrict a service to registered operations, and signs every request it sends your origin so the origin can refuse traffic that bypassed it. It does not rate limit, does not score query cost or complexity, and does not decide field-level authorization, so those stay in your API. The hardening guide lists what runs where.
Should I disable introspection in production?
It depends, and on its own it is not a security boundary. Disabling introspection makes it harder for an attacker to map your schema, but a determined one can still discover fields by probing, so treat it as reducing convenience for attackers rather than as real protection. Some teams keep it on for trusted internal tooling and off for public traffic. Either way, do not rely on a hidden schema in place of proper authorization and query controls, which are what actually stop abuse. GraphPilot makes the decision once, in front of your API: introspection is blocked at the edge by default and can be allowed per service, so the same rule applies to every request rather than depending on how each server is configured.
How do I stop deeply nested or abusive queries from overloading my API?
Put limits on what a single query is allowed to do. Maximum depth stops deeply nested or recursive relationship traversals, complexity or cost limits cap the total work a query can request, and limits on aliases and batching stop many operations being smuggled into one request to dodge rate limits. Combine these with rate limiting by cost, so one heavy request counts for more than many cheap ones. The key is that all of this is decided by what is inside the query body, so it has to be enforced by something that parses GraphQL, not by a generic perimeter tool. GraphPilot parses every query at the edge and refuses one that is too deep (10 levels by default), too large, or has too many selections before it reaches your origin, and it can accept only operations you registered. It does not compute a cost score or rate limit by cost, so a shallow query over a large list still reaches your origin; keep cost limits and rate limiting in your API or in front of GraphPilot. See hardening a public API.
How do I handle authentication and authorization in GraphQL?
Keep the two separate. Authentication, proving who the caller is, belongs at the edge of your API where you validate a token or session once per request. Authorization, deciding what that caller may see, has to be enforced per field, because a single query can touch many types with different rules. Centralize the policy so the same rules are applied everywhere rather than copied into each resolver, and default to denying access so a new field is not accidentally public. Enforcing authorization consistently is what prevents fields from quietly returning data the viewer should not see. GraphPilot handles the authentication half at the edge: it verifies JWTs against your keys and refuses a token that does not verify before your origin is called, recognizes opaque API keys so their traffic stays cacheable (verifying those stays with your origin), and keeps one caller's cached response from being served to another. Field-level authorization stays in your resolvers, because only your domain logic can decide it.
Can I enforce GraphQL security without touching every resolver?
To a large extent, yes. Depth and size limits, operation allowlists, introspection control and token verification are all decided by inspecting the request, so they can be enforced at the boundary instead of being scattered across resolvers. GraphPilot is a GraphQL-native edge proxy that parses each query in front of your origin and applies these at the edge, so a refused query never reaches your servers. Rate limiting, query cost scoring and field-level authorization are not part of it, so you still own those in your API.

Pricing

Is there a free plan?
Yes. Glider (Free) is for side projects, hobby apps and trying GraphPilot out, with no credit card required. It is not meant for production traffic: it comes with no support commitment, and once its monthly request unit allowance is used up, edge caching and request logging pause until its monthly window resets. The paid plans Jet (Starter) and Airliner (Business) add larger allowances, more services, longer retention and deeper Insights. Team seats are unlimited on every plan. Pricing →
When does billing start?
Glider (Free) costs nothing and needs no credit card. A paid plan is billed from the moment you order it in the portal: its price is charged at once, in advance, monthly or yearly. Usage above the plan's allowance is billed monthly in arrears, also on a plan billed yearly. Pricing →
Can I change plans later?
Yes. You can start on Glider (Free) and move up to Jet (Starter) or Airliner (Business) as your GraphQL traffic and team needs grow. Jumbo (Enterprise) adds custom pricing and volumes, and a custom SLA and support. Pricing →
How many organizations and custom domains does a plan include?
Both are counted per billing account, across every organization it funds. Glider (Free) funds 1 organization and includes no custom domain, Jet (Starter) 2 organizations and 2 custom domains, Airliner (Business) 3 organizations and 5 custom domains. Jumbo (Enterprise) includes at least 5 custom domains, and its contract sets the organizations and any further domains. An account that already funds more organizations or holds more custom domains than its plan includes, for example after moving to a smaller plan, keeps them; only a new one is refused. Pricing →
Is there an audit log?
Yes, on every plan. Each organization has an audit log in the portal that shows who changed what and when: services, deployments, purges, history resets, signing keys, custom domains, plan and billing changes, members and API keys, settings and the acceptance of the terms. It names the member, the API key, GraphPilot support or GraphPilot itself that made the change; member and API key changes come from WorkOS, which manages them, and show WorkOS as the actor. It records no request contents and no secrets. Every member can view it, whatever their role; API keys never can, and no one can change or delete an entry. It reaches back 1 day on Glider (Free), 30 days on Jet (Starter), 180 days on Airliner (Business) and 365 days on Jumbo (Enterprise), and it is stored in the EU. Pricing →
What happens when Glider's request units run out?
Your API keeps working: requests are still authenticated, signed and forwarded to your origin. Edge caching and request logging pause together until Glider's monthly window resets or you move to a paid plan. The window starts when the account moves to Glider, at sign-up or after a paid plan ends, and repeats monthly from there: requests served in that time are counted but not recorded, so they do not appear in Insights and an upgrade does not bring them back. Glider has no separate Insights allowance: telemetry is recorded only while the request units last; once they are used up, the account serves in lean mode and records no telemetry. Upgrade before the allowance runs out if you need a record of every request. Glider is offered under fair use; see below. Pricing →
Does Glider ever stop serving?
Not because its allowance is used up: Glider keeps serving in lean mode. It is offered under fair use, though. If an account sends traffic far beyond what Glider is meant for, GraphPilot may suspend its services and lock the account until the start of the next month, or until it moves to a paid plan. A warning email usually goes out before that happens, and nothing is deleted. Paid plans have no such limit; their usage above the allowance is billed. Pricing →
What are request units?
Request units are what the request allowance and its overage are counted in. They are counted per request at each point of presence, and by response size. A request uses one request unit per started 10 KiB (10,240 bytes) of its uncompressed response body, at least one, at every point of presence it passes through. Headers are excluded, and the same query costs the same whatever the client's Accept-Encoding, so compression does not change the count. A response without a body uses one. Without shielding, a request passes through one point of presence: a 2 KB response is 1 request unit, a 25 KB response 3, and a 50 KB response, typical for a GraphQL API, 5. With shielding, a cache miss or a mutation goes on from the edge location that received it to the shield location you chose, so it passes through two, also when the shield takes too long to answer: a 2 KB response is 2 request units, a 25 KB response 6, a 50 KB response 10. A request the edge cannot send to the shield, and takes to your origin itself instead, passes through one. A background refresh of a stale copy through the shield is a request of its own. Purges through GraphPilot are free. Insights, and on paid plans its allowance of logged requests, count each logged request once, whatever its size or route; a service can log fewer requests or none (telemetry.record). Pricing →

Pre-flight boarding is open

Start on Glider (Free).
No credit card, no commitment.