NOTE: this project is looking for maintainers. If you need caching to work for newer versions of graphql-ruby or for connection types, you may want to have a look at the graphql-fragment_cache gem.
A modern, compatible caching plugin for graphql-ruby supporting both GraphQL 1.x and 2.x
This version has been completely modernized to support the latest GraphQL-Ruby versions while maintaining backward compatibility with 1.x applications.
- GraphQL-Ruby 2.x Support: Uses new field extensions instead of deprecated field instrumentation
- Backward Compatible: Seamlessly works with GraphQL-Ruby 1.8+ through 2.5+
- Smart Object Cleaning: Automatically handles non-serializable objects (Procs, Methods) in cached data
- Improved Connection Handling: Supports both GraphQL 1.x and 2.x connection types
- Enhanced Error Handling: Graceful fallbacks for serialization issues
| GraphQL-Ruby Version | graphql-cache Version | Status |
|---|---|---|
| 2.0.0 - 2.5.x | 1.0.0+ | β Fully Supported (Extensions) |
| 1.12.0 - 1.13.x | 1.0.0+ | β Fully Supported (Instrumentation) |
| 1.8.0 - 1.11.x | 1.0.0+ | β Fully Supported (Instrumentation) |
| < 1.8.0 | 0.6.1 |
- Provide resolver-level caching for GraphQL APIs written in ruby
- Configurable to work with or without Rails
- Modern compatibility with GraphQL-Ruby 1.x and 2.x
- Intelligent handling of complex objects and serialization edge cases
- API Documentation
At StackShare we've been rolling out graphql-ruby for several of our new features and found ourselves in need of a caching solution. We could have simply used Rails.cache in our resolvers, but this creates very verbose types or resolver classes. It also means that each and every resolver must define its own expiration and key. GraphQL Cache solves that problem by integrating caching functionality into the graphql-ruby resolution process making caching transparent on most fields except for a metadata flag denoting the field as cached. More details on our motivation for creating this here.
Add this line to your application's Gemfile:
gem 'graphql-cache'And then execute:
$ bundleOr install it yourself as:
$ gem install graphql-cache- Use GraphQL Cache as a plugin in your schema:
class MySchema < GraphQL::Schema
query Types::Query
use GraphQL::Cache
end- Add the custom caching field class to your base object class:
module Types
class Base < GraphQL::Schema::Object
field_class GraphQL::Cache::Field
end
endThe setup is identical to 2.x - the gem automatically detects your GraphQL-Ruby version and uses the appropriate caching mechanism (field extensions for 2.x, field instrumentation for 1.x).
Note: If you want access to the cache keyword param in interface fields, the field_class directive must be added to your base interface module as well.
GraphQL Cache can be configured in an initializer:
# config/initializers/graphql_cache.rb
GraphQL::Cache.configure do |config|
config.namespace = 'GraphQL::Cache' # Cache key prefix for keys generated by graphql-cache
config.cache = Rails.cache # The cache object to use for caching
config.logger = Rails.logger # Logger to receive cache-related log messages
config.expiry = 5400 # 90 minutes (in seconds)
endAny object, list, or connection field can be cached by simply adding cache: true to the field definition:
field :calculated_field, Int, cache: true
field :expensive_query, [Types::MyType], cache: true
field :connection_field, Types::MyType.connection_type, cache: trueBy default all keys will have an expiration of GraphQL::Cache.expiry which defaults to 90 minutes. If you want to set a field-specific expiration time pass a hash to the cache parameter like this:
field :calculated_field, Int, cache: { expiry: 10800 } # expires key after 180 minutesGraphQL Cache generates a cache key using the context of a query during execution. A custom key can be included to implement versioned caching as well. By providing a :key value to the cache config hash on a field definition. For example, to use a custom method that returns the cache key for an object use:
field :calculated_field, Int, cache: { key: :custom_cache_key }With this configuration the cache key used for this resolved value will use the result of the method custom_cache_key called on the parent object.
It is possible to force graphql-cache to resolve and write all cached fields to cache regardless of the presence of a given key in the cache store. This will effectively "renew" any existing cached expirations and warm any that don't exist. To use forced caching, add a value to :force_cache in the query context:
MySchema.execute('{ company(id: 123) { cachedField }}', context: { force_cache: true })This will resolve all cached fields using the field's resolver and write them to cache without first reading the value at their respective cache keys. This is useful for structured cache warming strategies where the cache expiration needs to be updated when a warming query is made.
GraphQL Cache automatically handles complex object serialization to ensure reliable caching. Here's what you need to know:
Basic Types:
field :string_field, String, cache: true # β
Strings
field :integer_field, Int, cache: true # β
Integers
field :float_field, Float, cache: true # β
Floats
field :boolean_field, Boolean, cache: true # β
Booleans
field :array_field, [String], cache: true # β
Arrays
field :hash_field, GraphQL::Types::JSON, cache: true # β
HashesActiveRecord Objects:
field :user, Types::UserType, cache: true # β
Single AR objects
field :users, [Types::UserType], cache: true # β
AR collections
field :user_connection, Types::UserType.connection_type, cache: true # β
ConnectionsGraphQL Objects:
field :custom_object, Types::MyCustomType, cache: true # β
Custom GraphQL types
field :interface_field, Types::MyInterface, cache: true # β
Interface typesThe gem automatically cleans these objects to make them cacheable:
Non-Serializable Callables:
# These are automatically replaced with nil during caching
field :field_with_proc, String, cache: true do
# Procs in resolved data are cleaned out
endActiveRecord with Associations Containing Procs:
# The gem extracts core attributes and IDs, removing problematic associations
field :user_with_complex_data, Types::UserType, cache: trueFile Objects:
field :file_field, String, cache: false # β File objects cannot be marshaled
# Use cache: false or implement custom serializationExternal API Clients:
field :api_client, String, cache: false # β HTTP clients, API connections
# Cache the result data, not the client objectDatabase Connections:
field :db_connection, String, cache: false # β Database connection objects
# Cache query results, not connection objects1. Cache Results, Not Clients:
# β Don't cache the client
field :api_data, String, cache: true do
external_api_client.fetch_data # Client gets cached (problematic)
end
# β
Cache the result
field :api_data, String, cache: true do
result = external_api_client.fetch_data
result.to_json # Only cache serializable data
end2. Use Custom Keys for Complex Objects:
field :complex_calculation, Float, cache: { key: :calculation_cache_key }
def calculation_cache_key
"calc_#{updated_at.to_i}_#{some_dependent_value}"
end3. Handle Large Objects:
# For very large objects, consider caching subsets
field :large_dataset, [Types::DataType], cache: { expiry: 3600 } do
# Cache expires sooner for large datasets
endEnable detailed cache logging in your configuration:
GraphQL::Cache.configure do |config|
config.logger = Rails.logger
# Set log level to debug to see cache hits/misses
endLog Output Examples:
DEBUG -- : Cache miss: (GraphQL::Cache:User:123:calculated_field:abc123)
DEBUG -- : Cache hit: (GraphQL::Cache:User:123:calculated_field:abc123)
DEBUG -- : Cache write successful after cleaning: (GraphQL::Cache:User:123:complex_field:def456)
DEBUG -- : Cache skip: (GraphQL::Cache:User:123:problematic_field:ghi789) - failed to serialize even after cleaning
Track cache effectiveness in your application:
# In your GraphQL context
context = {
cache_stats: { hits: 0, misses: 0 }
}
# The gem will populate these stats during execution
MySchema.execute(query, context: context)
puts "Cache hits: #{context[:cache_stats][:hits]}"
puts "Cache misses: #{context[:cache_stats][:misses]}"No breaking changes! The new version is fully backward compatible:
- Update your Gemfile:
gem 'graphql-cache', '~> 1.0'- Run bundle update:
bundle update graphql-cache- (Optional) For GraphQL-Ruby 2.x users, you can remove field instrumentation warnings by ensuring you're using the latest schema definition format.
If you're migrating from the fragment_cache gem:
# Before (fragment_cache)
field :expensive_field, String do
extension GraphQL::FragmentCache::ObjectCacheExtension, cache_key: :cache_key
end
# After (graphql-cache)
field :expensive_field, String, cache: { key: :cache_key }After checking out the repo, run bin/setup to install dependencies. Then, run rake spec to run the tests. You can also run bin/console for an interactive prompt that will allow you to experiment.
Testing with Different GraphQL Versions:
# Test with GraphQL 1.x
bundle exec appraisal graphql-1-13 rspec
# Test with GraphQL 2.x
bundle exec appraisal graphql-2-0 rspecTo install this gem onto your local machine, run bundle exec rake install.
Bug reports and pull requests are welcome on GitHub at https://github.com/stackshareio/graphql-cache. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the Contributor Covenant code of conduct.
When reporting serialization or caching issues, please include:
- GraphQL-Ruby version
- Example field definition
- Sample object structure that's failing to cache
- Complete error logs with debug logging enabled
The gem is available as open source under the terms of the MIT License.
Everyone interacting in the graphql-cache project's codebases, issue trackers, chat rooms and mailing lists is expected to follow the code of conduct.
