Managed GraphQL edge cache

GraphQL Edge Caching cuts your origin load.

GraphPilot cuts your origin load, makes your app faster, clears what a mutation changed and needs only a new endpoint.

Drop GraphPilot in front of your GraphQL API. Repeat queries answer from the edge.

Prefer to talk first? Book a call

Works in front of any GraphQL server, gateway or federated supergraph.
Free to start, no credit card required.

Watch the full explainer (8 min) (opens YouTube in a new tab)

Response sizeA single record
GraphPilot cache hit, p959 ms

measured p95 cache hit
How it was measured

The problem

Caching GraphQL is easy. Keeping it correct isn't.

Put a normal CDN in front of GraphQL and three problems surface at once.

Read the docs

One URL

A CDN sees one blind endpoint

Every query and mutation shares one URL, with the operation and variables buried in the request. A CDN can't tell them apart, so read-heavy APIs hammer the origin all day.

Stale data

A clock can't tell what changed

Short TTLs throw away the hit rate; long TTLs serve stale data. A clock can't say which mutation changed which result, so you either under-cache or risk correctness.

1 → N caches

Invalidation is a distributed system

A single write invalidates entities, lists and derived results across many cached responses. Hand-written purge rules are fragile and rarely stay correct.

cache-hit benchmark
measured
9–18 ms
p95 cache-hit response time
4 sizes
responses from 101 B to 381 KB
3 regions
load from Europe, US East and Asia

A reproducible demo benchmark, not a guaranteed service level. How it was measured

Why we built it

There's already one. So why another one?

We set out to cache a GraphQL API at the edge and kept hitting the same wall: caching was never the hard part, keeping it correct was. So we built the layer we wanted.

Where we go further

See how it caches, right in the schema.

schema.graphql

the schema you write

type Product @cacheRule(maxAge: 300) {
id: ID! @surrogateKey
name: String!
price: Float!
}

the query, keyed either way

# a client omits the key
{ product(id: "42") { name } }
# GraphPilot injects it at the edge
{ product(id: "42") { id name } }

The backend team owns it

Cache and invalidation are directives next to the fields they govern, so the team that owns a type owns how it caches. No central config to coordinate.

And can rely on it

Forget the id in a query and caching still holds: GraphPilot injects it, so every cached response stays invalidatable.

  • Built for federated graphs

    In front of any gateway, we cache a federated graph as one. First-class, not an afterthought.

  • Empty lists stay correct

    An empty result is a real cache entry, cleared when a mutation touches its type.

  • Purge on demand, too

    When a change bypasses GraphQL, purge by key or everything from the API, the CLI or the Cockpit.

  • Drop it in, keep your stack

    No new gateway, no client code changes: clients only switch their endpoint. It sits in front of the API you already run.

  • Invalidation you can verify

    See exactly which cache tags a mutation clears. Not a black box.

  • German company, EU telemetry

    A data processing agreement under Art. 28 GDPR, and request telemetry stored in the EU for at most 90 days.

How it works

One layer in front of your API. Here's how it earns its keep.

GraphPilot is a GraphQL-native proxy, written in Rust. It sits in front of your API and caches it for you, reading your schema to decide what to cache, how long to hold it, and what to clear when your data changes.

Client

Web · Mobile · Server

Request
Response
GraphPilot
Request
Response

Origin

GraphQL · REST · HTTP

Cache hits answer at the edge. Only misses and writes reach your origin.

Precise, verifiable invalidation

Mutations invalidate automatically.

Send a mutation through GraphPilot and it purges the cached responses that contained the objects it returned, usually within seconds at every edge location, and shows you which keys it cleared. Data changed elsewhere you purge through the API or CLI, and when a mutation doesn't reveal everything it changed, the behaviours below close the gap, so you decide what a change clears.

Read the docs
# changes a product's price
mutation UpdatePrice {
updateProduct(id: "42") {
price
}
}
every response fresh

Cached responses

The mutation only invalidates the entries it touches; each refills on its next request, not from the mutation.

When the response says nothing at all

Some mutations hide what they changed.

GraphPilot reads a mutation's response to know which entities to clear. But some mutations return only success: true and reveal nothing. Your origin can list the affected entities as cache tags in the response header, and GraphPilot purges exactly those. No schema change, no API call.

POST /graphql 200 OK
content-typeapplication/json
cache-controlpublic, max-age=300
age0
surrogate-keyProduct:42 Product:88 Category:sale
Purgedexact match · nothing else cleared
Product:42Product:88Category:sale
Programmatic invalidation

Changed elsewhere? Purge via API.

When a change happens somewhere GraphPilot can't see, an event on your bus, a cron job, an admin action in another system, call the purge API with the keys to clear. Full, precise control whenever you need it.

Read the docs

Event-based

Purge from your event bus the moment upstream data changes, with no GraphQL mutation involved.

Scheduled

Expire a set of keys on a cron or a job when a batch import or recompute finishes.

Cross-system

An admin action in another service clears exactly the right cached reads.

Precise or broad

Purge exact cache tags, or clear everything of a type by typename.

api.graphpilot.io/purge

a change from your system

Kafka eventcalls the purge API
POST https://api.graphpilot.io/purge
{ "serviceId": "my-shop", "keys": ["product:42"] }
200purge accepted
No missed invalidations

Your clients forget the id? GraphPilot doesn't.

When a query skips a key field, most caches still store the response but can't clear it when the data changes. GraphPilot injects the keys your schema needs, so every response stays precisely invalidatable, one entity or a whole group.

Read the docs

The query your client sends

injected by GraphPilot
query {
product(id: "42") {
id
sku
name
reviews(first: 3) {
rating
}
}
}
Example surrogate keys GraphPilot generates
Product:id:42Product:sku:CH-042

One product, keyed by its id and its SKU, so either one invalidates it.

Spike-proof origin

A thousand requests, one origin fetch.

When a spike sends the same query all at once, GraphPilot collapses them into a single origin request. The first one fetches; the rest wait and share its result, so nothing stampedes your origin. Your database feels one hit, not a thousand, and stays calm exactly when traffic doesn't.

Tablet
Laptop
Desktop
GraphPilot
Origin
1,000 identical requests 1 origin fetch
Dimension-aware caching

One query. Many dimensions.

Personalized responses stay cacheable. Radiogramm, a small expression language built into GraphPilot, reads straight from each request (a JWT claim, a header, a cookie) and derives a cache key from it. Write one line, and the same query splits into the buckets your rule describes, each filed under its own key. Base a split that keeps data apart on verified token claims, since headers and cookies are what the caller sent.

Prefer to decide at the origin? Return a Vary response header and the same split happens server-side, no rule required.

Read the docs
jwt.sub ?? "public" // one bucket per signed-in userRadiogramm
u_8f3kAda Lovelace{ cart: 3 }
u_2p9xGrace Hopper{ cart: 1 }
publicsigned out{ cart: 0 }

one query · separate buckets · no bucket sees another's data

Global edge

Served from the nearest edge location.

Repeat reads are answered close to your users on a global network, so felt speed holds up worldwide, not just next to your origin.

Read the docs
Single POP Multiple POPs Planned new POP Planned upgrade
Map reflects the Fastly network GraphPilot deploys to, current as of June 8, 2026. Subject to change. Source: Fastly .
Fastly is a trademark of Fastly, Inc. GraphPilot is not affiliated with or endorsed by Fastly.
Build vs buy

You could build this. Then you own it forever.

Storing a GraphQL response takes an afternoon. Keeping every cached read correct as data changes, and as your schema keeps evolving, is a distributed-systems problem you would maintain for the life of the product.

Build it yourself

The hidden work
  • Write purge rules for every mutation, entity, list and derived result.
  • Handle the edge cases: empty lists, missing ids, partial selections.
  • Re-verify correctness every time the schema changes.
  • Operate it: monitoring, staleness bugs, on-call.

weeks to build, then permanent maintenance

Drop in GraphPilot

Handled for you
  • Invalidation derived from your schema, automatically.
  • Empty lists, key-field injection and partial selections handled.
  • Policy updates ship in the same pull request as the schema.
  • A managed edge, with no purge system of your own to run.

drop it in front, keep shipping features

Choosing between managed GraphQL caches? See how GraphPilot and Stellate compare, with a source for every statement.

Get started

Stop choosing between fast and fresh.

Put GraphPilot in front of your GraphQL API and get edge speed with invalidation you can actually trust.

Prefer to talk first? Book a call