Persisted queries allow you to cache compiled GraphQL queries, significantly improving performance by avoiding repeated parsing and compilation of the same queries.
How Persisted Queries Work
Rails GraphQL can compile a query once and store it in a cache. Subsequent requests can reference the cached version using a unique key instead of sending the full query string.
Benefits
- Performance: Avoid parsing and compiling the same query multiple times
- Reduced bandwidth: Send only a cache key instead of the full query
- Security: Optionally restrict to only pre-approved queries
Compiling Queries
Use the compile method to compile a GraphQL query for caching:
query = GraphQL.compile('{ user(id: 1) { name email } }', schema: AppSchema)
The compiled query is serialized using Marshal.dump and optionally compressed with Zlib.deflate:
# Compile without compression
query = GraphQL.compile('{ users { id name } }', schema: AppSchema, compress: false)
# Compile with compression (default)
query = GraphQL.compile('{ users { id name } }', schema: AppSchema)
Executing Compiled Queries
Once compiled, execute the query using the compiled: true option:
query = GraphQL.compile('{ user(id: 1) { name } }', schema: AppSchema)
result = GraphQL.execute(query, compiled: true, schema: AppSchema)
Cache Integration
Rails GraphQL provides cache integration methods on the schema:
Checking Cache
AppSchema.cached?(cache_key)
Writing to Cache
compiled_query = GraphQL.compile('{ users { id name } }', schema: AppSchema)
AppSchema.write_on_cache(cache_key, compiled_query)
Reading from Cache
query = AppSchema.read_from_cache(cache_key)
result = GraphQL.execute(query, compiled: true, schema: AppSchema)
Fetching with Fallback
query = AppSchema.fetch_from_cache(cache_key) do
GraphQL.compile('{ users { id name } }', schema: AppSchema)
end
Deleting from Cache
AppSchema.delete_from_cache(cache_key)
Cache Keys
Rails GraphQL supports CacheKey objects for versioned cache keys:
cache_key = Rails::GraphQL::CacheKey.new('my-query', '1.0')
AppSchema.write_on_cache(cache_key, compiled_query)
Cache keys are automatically prefixed with the schema’s namespace:
# For a schema with namespace :api
AppSchema.send(:expand_cache_key, 'user-query')
# => 'graphql/api/user-query'
Complete Example
Here’s a complete example implementing persisted queries:
class GraphQL::AppSchema < GraphQL::Schema
namespace :api
query_fields do
field(:user, 'User').argument(:id, :id, null: false)
field(:users, 'User', array: true)
end
end
Store a compiled query:
# Generate a unique cache key
cache_key = "user-query-#{SecureRandom.uuid}"
# Compile and cache the query
compiled = GraphQL.compile('{ users { id name email } }', schema: GraphQL::AppSchema)
GraphQL::AppSchema.write_on_cache(cache_key, compiled)
Retrieve and execute:
# Later, retrieve and execute the cached query
if GraphQL::AppSchema.cached?(cache_key)
query = GraphQL::AppSchema.read_from_cache(cache_key)
result = GraphQL.execute(query, compiled: true, schema: GraphQL::AppSchema)
else
# Fallback to regular execution
result = GraphQL.execute('{ users { id name email } }', schema: GraphQL::AppSchema)
end
Cache-Only Execution
You can execute using only a cache key without providing the query string:
# Execute using only the cache key
result = GraphQL.execute(nil, hash: cache_key, schema: GraphQL::AppSchema)
This requires the query to already be in the cache, or it will fail.
Configuration
Cache Store
The cache store is configured through the schema’s config:
class GraphQL::AppSchema < GraphQL::Schema
configure do |config|
config.cache = ActiveSupport::Cache::MemoryStore.new
end
end
By default, Rails GraphQL uses the Rails cache.
Cache Prefix
Each schema gets an automatic cache prefix based on its namespace:
class GraphQL::AppSchema < GraphQL::Schema
namespace :api
end
# Cache keys are prefixed with 'graphql/api/'
GraphQL::AppSchema.config.cache_prefix # => 'graphql/api/'
You can customize the prefix:
class GraphQL::AppSchema < GraphQL::Schema
configure do |config|
config.cache_prefix = 'my-custom-prefix/'
end
end
Best Practices
Generate stable cache keys
Use deterministic cache keys based on query content or user permissions. Consider using a hash of the query string.
Set appropriate TTLs
Configure cache expiration based on how frequently your schema changes. Longer TTLs improve performance but may serve stale schemas.
Monitor cache hit rates
Track how often queries are served from cache vs. compiled fresh to optimize your caching strategy.
Handle cache misses gracefully
Always have a fallback to compile and execute queries when the cache is unavailable or empty.
Ensure your cache keys are unique per query and per schema version to avoid serving incorrect cached results.
When to Use Persisted Queries
Use persisted queries when:
- You have frequently repeated queries
- Query compilation is a performance bottleneck
- You want to reduce client-to-server bandwidth
- You need to allowlist approved queries
Skip persisted queries when:
- Queries are highly dynamic and rarely repeated
- Cache overhead exceeds compilation cost
- You need real-time schema reflection
Compression Trade-offs
Compression reduces storage size but adds CPU overhead:
# Larger in cache, faster to retrieve
GraphQL.compile(query, schema: AppSchema, compress: false)
# Smaller in cache, slower to retrieve
GraphQL.compile(query, schema: AppSchema, compress: true)
Choose compression based on your cache storage limits and CPU availability.