Caching
Caching — Response caching
Automatic Persisted Queries (APQ) reduce bandwidth and improve latency by caching query strings on the server.
Instead of sending the full query text on every request, clients send a query hash:
First Request: The client sends the query along with its hash. The server caches the query and returns data.
Subsequent Requests: The client sends only the hash (much smaller). The server looks up the query from cache and returns data.
Without APQ, every request carries the full query string:
# Without APQ — full query on every request (~1.2 KB body)curl -s -X POST https://api.example.com/graphql \ -H "Content-Type: application/json" \ -d '{ "query": "query GetUserProfile($id: ID!) { user(id: $id) { id name email createdAt orders(last: 5) { id total status } } }", "variables": { "id": "user-123" } }'With APQ, after the first request only the 64-byte hash is sent:
# With APQ — hash only (~120 byte body, 90%+ smaller)curl -s -X POST https://api.example.com/graphql \ -H "Content-Type: application/json" \ -d '{ "extensions": { "persistedQuery": { "version": 1, "sha256Hash": "a1b2c3d4e5f6789012345678901234567890123456789012345678901234abcd" } }, "variables": { "id": "user-123" } }'
# Response (same data, no query overhead):# { "data": { "user": { "id": "user-123", "name": "Alice", ... } } }APQ is enabled by default with an in-memory LRU cache. No TOML configuration is required for single-instance deployments.
No configuration needed. The server caches up to 1,000 query strings in memory with a 1-hour TTL.
To disable APQ (e.g., in testing), set the environment variable:
FRAISEQL_APQ_ENABLED=false fraiseql runFor deployments behind a load balancer, use the Redis backend via the redis-apq Cargo feature. Set REDIS_URL in your environment — FraiseQL picks it up automatically when the feature is enabled:
REDIS_URL=redis://your-redis:6379 fraiseql runAPQ uses extensions to send the query hash:
{ "extensions": { "persistedQuery": { "version": 1, "sha256Hash": "abc123def456..." } }, "variables": { "id": "user-123" }}PersistedQueryNotFound errorimport { ApolloClient, InMemoryCache } from '@apollo/client';import { createPersistedQueryLink } from '@apollo/client/link/persisted-queries';import { createHttpLink } from '@apollo/client/link/http';import { sha256 } from 'crypto-hash';
const httpLink = createHttpLink({ uri: '/graphql' });const persistedQueriesLink = createPersistedQueryLink({ sha256 });
const client = new ApolloClient({ cache: new InMemoryCache(), link: persistedQueriesLink.concat(httpLink)});import { createClient, fetchExchange } from 'urql';import { persistedExchange } from '@urql/exchange-persisted';
const client = createClient({ url: '/graphql', exchanges: [ persistedExchange({ preferGetForPersistedQueries: true }), fetchExchange ]});const sha256 = require('crypto-js/sha256');
async function executeQuery(query, variables) { const hash = sha256(query).toString();
// Try with hash only let response = await fetch('/graphql', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ extensions: { persistedQuery: { version: 1, sha256Hash: hash } }, variables }) });
let result = await response.json();
// If not found, send with full query if (result.errors?.[0]?.message === 'PersistedQueryNotFound') { response = await fetch('/graphql', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ query, extensions: { persistedQuery: { version: 1, sha256Hash: hash } }, variables }) }); result = await response.json(); }
return result;}You can test the full APQ flow manually to verify your configuration:
# Step 1: Attempt hash-only request (expect PersistedQueryNotFound)curl -s -X POST http://localhost:8080/graphql \ -H "Content-Type: application/json" \ -d '{ "extensions": { "persistedQuery": { "version": 1, "sha256Hash": "ecf4edb46db40b5132295c0291d62fb65d6759a9aeacfe1f108c30a4740c5f71" } } }'# Response: {"errors":[{"message":"PersistedQueryNotFound","extensions":{"code":"PERSISTED_QUERY_NOT_FOUND"}}]}
# Step 2: Send hash + query to register itcurl -s -X POST http://localhost:8080/graphql \ -H "Content-Type: application/json" \ -d '{ "query": "{ __typename }", "extensions": { "persistedQuery": { "version": 1, "sha256Hash": "ecf4edb46db40b5132295c0291d62fb65d6759a9aeacfe1f108c30a4740c5f71" } } }'# Response: {"data":{"__typename":"Query"}}
# Step 3: Now hash-only works (query is cached)curl -s -X POST http://localhost:8080/graphql \ -H "Content-Type: application/json" \ -d '{ "extensions": { "persistedQuery": { "version": 1, "sha256Hash": "ecf4edb46db40b5132295c0291d62fb65d6759a9aeacfe1f108c30a4740c5f71" } } }'# Response: {"data":{"__typename":"Query"}}With APQ, cached queries can be sent as GET requests:
GET /graphql?extensions={"persistedQuery":{"version":1,"sha256Hash":"abc123"}}&variables={"id":"123"}FraiseQL verifies that the query matches the hash when both are sent together:
{ "query": "{ users { id } }", "extensions": { "persistedQuery": { "version": 1, "sha256Hash": "wrong-hash-value-here" } }}Response:
{ "errors": [{ "message": "Provided sha does not match query", "extensions": { "code": "BAD_USER_INPUT" } }]}APQ caching is additive by default — any query can be registered on first use. For deployments where you want to restrict execution to known queries only, use the redis-apq feature flag in combination with infrastructure-level controls (e.g., only pre-seeding the Redis cache before deploy and blocking the registration flow via a reverse proxy).
APQ queries are registered automatically on first use — no batch upload API is needed. To warm the cache after a deploy, run your queries once against the live server:
# Warm cache by executing each query once after deployfor hash in $(cat queries.json | jq -r 'keys[]'); do query=$(cat queries.json | jq -r --arg h "$hash" '.[$h]') curl -s -X POST $FRAISEQL_URL/graphql \ -H "Content-Type: application/json" \ -d "{\"query\": $(echo "$query" | jq -R -s '.'), \ \"extensions\": {\"persistedQuery\": {\"version\": 1, \"sha256Hash\": \"$hash\"}}}" \ > /dev/nulldoneMonitor APQ performance:
| Metric | Description |
|---|---|
fraiseql_apq_hits_total | Cache hits |
fraiseql_apq_misses_total | Cache misses |
fraiseql_apq_stored_total | Queries stored |
fraiseql_apq_redis_errors_total | Redis backend errors (only present with redis-apq feature) |
# APQ hit ratesum(rate(fraiseql_apq_hits_total[5m])) /(sum(rate(fraiseql_apq_hits_total[5m])) + sum(rate(fraiseql_apq_misses_total[5m])))
# Queries stored over timerate(fraiseql_apq_stored_total[5m])Typical GraphQL queries are 1–10 KB. Hashes are 64 bytes.
| Query Size | With APQ | Savings |
|---|---|---|
| 1 KB | 64 B | 94% |
| 5 KB | 64 B | 99% |
| 10 KB | 64 B | 99% |
After deploying, run your queries once against the live server so subsequent requests can use the hash-only path:
- name: Warm APQ cache run: npm run warm-apq-cache # script that POSTs each query+hash once to $FRAISEQL_URL/graphqlTarget greater than 90% hit rate in production:
fraiseql_apq_hits_total / (fraiseql_apq_hits_total + fraiseql_apq_misses_total) > 0.9The in-memory APQ store uses an LRU cache with a default capacity of 1,000 query entries. This is not currently configurable via TOML or environment variables — if you need a larger cache, use the redis-apq feature flag (Redis has no fixed capacity limit).
Single-instance memory cache doesn’t share across servers. Enable the redis-apq Cargo feature and set REDIS_URL:
REDIS_URL=redis://your-redis:6379 fraiseql run# Check APQ hit/miss counts via the metrics endpointcurl -s http://localhost:8080/metrics | grep fraiseql_apq
# Test manually: send hash + query to register, then hash-only to verifycurl -s -X POST http://localhost:8080/graphql \ -H "Content-Type: application/json" \ -d '{"query":"{__typename}","extensions":{"persistedQuery":{"version":1,"sha256Hash":"ecf4edb46db40b5132295c0291d62fb65d6759a9aeacfe1f108c30a4740c5f71"}}}'Ensure client and server use the same hashing:
APQ (Automatic Persisted Queries) is GraphQL-only. REST endpoints are identified by path, gRPC endpoints by service/method name — neither uses query hashing. APQ configuration has no effect on REST or gRPC transports.
Caching
Caching — Response caching
Performance
Performance — Additional optimizations
Deployment
Deployment — Production configuration