Open-source project
anycable/graphql-anycable avatar
anycable/graphql-anycable

graphql-anycable: GraphQL subscriptions without a Ruby process in the fan-out path

A drop-in replacement for GraphQL ActionCable subscriptions. Works with AnyCable.

114 stars20 forksRubyMIT

At a glance

What is it?
A drop-in replacement for the subscriptions adapter in the graphql gem, built for AnyCable, which is fast precisely because it executes no Ruby. The price is that subscription state lives in Redis and expires, so a cleanup rake task becomes part of your operations rather than an afterthought.
Who is it for?
Reach for graphql-anycable if you are already running AnyCable, because the alternative is that subscriptions do not work there at all, and the adapter swap is small. Leave it if you are on Action Cable with no plan to move, since the default adapter needs no Redis and no cleanup job.
Can I use it commercially?
Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
Is it still maintained?
Yes. The repository last received commits 42 days ago.
What is it written in?
Mainly Ruby, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on October 1, 2026, and from our analysis. They are not legal advice.

Editorial analysis

The incompatibility is architectural, not a missing feature

The reason this gem exists is a direct collision between two designs.

AnyCable is fast because it does not execute any Ruby code. The default subscription implementation in the graphql gem requires exactly that: it re-evaluates the GraphQL query inside the Action Cable process. AnyCable does not support this, and the documentation is candid that it would be possible but hard to implement. The long-running discussion it points to, on the anycable-rails issue tracker, is where the reasoning is laid out.

The workaround this gem takes is to move the query evaluation out of the fan-out path entirely. Two differences from the default adapter follow, and both are listed as differences rather than buried.

Subscription information is stored in a Redis database, by default using the AnyCable Redis configuration, and expiry or data cleanup has to be configured separately. And GraphQL queries for all subscriptions are re-executed in the process that triggers the event, which may be the web server, an async job, a rake task, or something else.

That second point is the one to internalise. Evaluation happens once at the event, not once per subscriber, which is the whole point, and it means the triggering process needs the application context to do it correctly.

The swap is one line in the schema

Installation is a Gemfile entry and a bundle:

ruby
gem "graphql-anycable", "~> 1.0"
sh
bundle install

Then the plugin goes in the schema, replacing the Action Cable adapter if you had one:

ruby
class MySchema < GraphQL::Schema
  use GraphQL::AnyCable, broadcast: true

  subscription SubscriptionType
end

The rest is a normal subscription flow. You execute the query inside a channel, and the channel class is where the query result gets transmitted back. The trigger call is the standard graphql-ruby subscriptions API, with a topic name, a payload, a subject, and a scope:

ruby
MySchema.subscriptions.trigger(:product_updated, {}, Product.first!, scope: account.id)

One wiring detail is easy to miss and produces a confusing failure. You have to pass the channel instance as the channel key in the context, because that is how the adapter knows which subscription to attach a result to.

The teardown matters too: the channel's unsubscribed callback calls delete_channel_subscriptions on the schema, so leaving that out leaks subscriptions. Recent releases have been about exactly this, with 1.3.3 fixing leaking channel subscriptions and optimising their cleaning, and 1.3.4 fixing expired subscription handling.

Compatibility is broader than Rails. It works with Action Cable for development and test, and without Rails at all through LiteCable.

Broadcast is off by default per field, for a reason

Without broadcasting, the gem evaluates the query and transmits the result to every subscription client individually. The documentation calls that a waste of resources when hundreds or thousands of clients are subscribed to the same data, and says it has a huge negative impact on performance.

The fix is the Subscriptions Broadcast feature in GraphQL-Ruby, which groups identical subscriptions, executes them, and transmits once. You enable it with the broadcast option, which the schema example above already sets to true.

What is not enabled by default is the assumption that any given field is safe to share. Every field starts marked as not safe for broadcasting, and if a subscription query includes even one non-broadcastable field, GraphQL-Ruby falls back to executing every subscription independently for every client. That is a per-subscription cost with a per-subscription cliff, and it is why a schema can appear to broadcast and then not.

The escape hatch exists and is marked dangerous. Setting default_broadcastable to true marks everything as safe, and the documentation appends a warning in the same sentence that it can have security implications. That is the correct warning: broadcasting a field that varies per user, such as anything scoped to an account, sends one client's data to everyone sharing that subscription.

ruby
class MySchema < GraphQL::Schema
  use GraphQL::AnyCable, broadcast: true, default_broadcastable: true

  subscription SubscriptionType
end

The per-field marking is the safe default, and it is the one you should keep.

Subscription state in Redis is a keyspace you now own

Moving state out of process memory is what makes the AnyCable fan-out work, and it converts a memory problem into an operations problem.

The operational consequence is named directly: to avoid filling Redis with stale subscription data, you set subscription_expiration_seconds to a number of seconds, for example 604800 for a week, and you run the cleanup task periodically:

sh
rake graphql:anycable:clean

There is a Heroku-specific carve-out. Heroku users are told to set use_redis_object_on_cleanup to false, because of documented limitations in Heroku Redis around connection permissions. The mechanism behind the setting is a Redis object during iteration, which some managed Redis tiers will not allow.

The cleanup task is also split into four sub-tasks, which is the part that matters at scale: rake graphql:anycable:clean:channels, clean:subscriptions, clean:fingerprint_subscriptions and clean:topic_fingerprints. You can run only some of them, or schedule them separately, which is how you avoid one long task holding a slot.

The implementation detail is given too, because it affects your Redis configuration. Cleanup iterates with the SCAN family of commands and pipelines the checks per batch, and redis_scan_count tunes the batch size, where larger means fewer round trips and more work per call.

And there is an escape hatch for the bad day: a section on emergency actions for clearing subscriptions when the expiry setting was never set and the cleaner is not keeping up.

Three configuration routes, and the filename trap

The gem configures itself through anyway_config, which means the three usual routes all work.

Environment variables are prefixed, and the naming is mechanical:

.env
GRAPHQL_ANYCABLE_SUBSCRIPTION_EXPIRATION_SECONDS=604800
GRAPHQL_ANYCABLE_USE_REDIS_OBJECT_ON_CLEANUP=true
GRAPHQL_ANYCABLE_REDIS_PREFIX=graphql
GRAPHQL_ANYCABLE_REDIS_SCAN_COUNT=1000

YAML goes in a file with a name that is easy to get wrong. It is config/graphql_anycable.yml and explicitly not config/anycable.yml, which is AnyCable's own file. A production block example sets subscription_expiration_seconds to 300, use_redis_object_on_cleanup to false for restricted Redis installations, redis_prefix to graphql, and redis_scan_count to 1000.

Application code is the third route, and it is where you would put anything computed:

ruby
GraphQL::AnyCable.configure do |config|
  config.subscription_expiration_seconds = 3600 # 1 hour
  config.redis_prefix = "graphql" # on our side, we add `-` ourselves after the redis_prefix
end

That last comment is worth internalising. The library appends its own separator after the prefix, so a trailing character in your prefix value produces a double separator in your key names. It is a small thing that shows up as keys you cannot find with a pattern match.

One more Redis setting is required in a specific case. If you use an AnyCable broadcasting adapter other than Redis, you must configure Redis for this gem yourself, either as a Redis instance or as a Proc that checks one out of a connection pool.

The keyspace prefix is yours, which is how you namespace a shared Redis

The redis_prefix setting deserves its own look because it is the one that prevents collisions in a shared Redis, and the documentation explains what it is for in the YAML example: configuring the prefix for anycable-graphql subscription prefixes, with a default value of graphql.

That default is a bare word. If two applications on the same Redis both use this gem and neither sets a prefix, they share key names. Since the keys hold subscription fingerprints and channel data keyed by scope, a collision means one application can clean up or overwrite the other's subscriptions, and the symptom would be subscriptions randomly dropping in an application you were not debugging.

Setting redis_prefix per application is therefore not a nicety. The value goes through the same three configuration routes, so an environment variable per deploy is the cheapest fix, and the library appending its own separator after whatever you supply means you do not need to add one.

The repository structure is small, which fits a gem. Alongside the README, CHANGELOG and licence there is the gemspec, a Rakefile, a Gemfile, a gemfiles directory for Appraisal-style matrix testing, lib, spec, bin, a .rspec and a rubocop configuration with its own directory. The default branch is master rather than main, and the licence is MIT.

Editorial conclusion

Reach for graphql-anycable if you are already running AnyCable, because the alternative is that subscriptions do not work there at all, and the adapter swap is small. Leave it if you are on Action Cable with no plan to move, since the default adapter needs no Redis and no cleanup job. Verify first that your subscription fields are genuinely safe to broadcast before setting default_broadcastable, because the documentation flags that option as having security implications, and set subscription_expiration_seconds from day one rather than discovering the problem through a full Redis keyspace.

Frequently asked questions

What does the graphql-anycable gem do?

It replaces the subscriptions adapter that ships with the graphql gem so GraphQL subscriptions work with AnyCable, which does not execute Ruby code. Subscription state is kept in Redis and the query is evaluated once in the process that triggers the event rather than once per subscriber.

How do I install graphql-anycable?

Add gem "graphql-anycable", "~> 1.0" to your Gemfile and run bundle install. Then plug it into the schema with use GraphQL::AnyCable, broadcast: true, and make sure you pass the channel instance as the channel key in the context.

What are GraphQL subscriptions used for?

In this gem they carry live updates to a connected client. A trigger call names a topic, a payload, a subject and a scope, and every subscription matching that scope receives the re-executed query result, either as a broadcast shared across identical subscriptions or individually per client.

How do I clean up stale subscriptions in graphql-anycable?

Set subscription_expiration_seconds and run rake graphql:anycable:clean periodically. The task is also split into clean:channels, clean:subscriptions, clean:fingerprint_subscriptions and clean:topic_fingerprints so you can schedule them separately, and redis_scan_count tunes the SCAN batch size.

Where does the graphql-anycable YAML config file go?

In config/graphql_anycable.yml, and explicitly not in config/anycable.yml, which is AnyCable's own configuration file. Environment variables prefixed with GRAPHQL_ANYCABLE_ and a GraphQL::AnyCable.configure block are the two other supported routes, all provided by anyway_config.

Is default_broadcastable safe to enable in graphql-anycable?

The documentation warns it can have security implications. By default every field is marked not safe for broadcasting, and a subscription with even one non-broadcastable field falls back to per-client execution. Setting it true marks all fields shareable, which means a field that varies per user could send one client's data to others.

Official sources

  1. Official README
  2. Project repository
  3. Release notes
Add this badge to your README

If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/anycable-graphql-anycable.svg)](https://hysenlabs.com/projects/anycable-graphql-anycable)