circuitbox stops calling a Ruby service that is already failing
Circuit breaker built with large Ruby apps in mind.
At a glance
- What is it?
- circuitbox is a Ruby circuit breaker gem for applications with many service dependencies. It tracks failures over a time window, opens the circuit when the error rate passes a threshold, and returns nil or raises depending on how you call it, with a Moneta-backed store for sharing state between processes.
- Who is it for?
- Ruby teams whose services call flaky dependencies should use circuitbox, because the default behaviour of tracking failures in one minute intervals and opening the circuit for 90 seconds is exactly the graceful degradation pattern it was written for, and the per-circuit thresholds are explicit rather than implicit.
- Can I use it commercially?
- Check first. The repository uses a licence we do not classify automatically, so read its LICENSE file before any commercial use.
- Is it still maintained?
- Yes. The repository last received commits 6 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 24, 2026, and from our analysis. They are not legal advice.
Editorial analysis
The defaults, and what a tripped circuit does
Circuitbox is a Ruby circuit breaker gem. It protects your application from failures of its service dependencies. It wraps calls to external services and monitors for failures in one minute intervals.
The default thresholds are stated in the README in one sentence, and they are worth reading carefully because they define the behaviour you get before you configure anything. Using a circuit's defaults, once more than 5 requests have been made with a 50% failure rate, Circuitbox stops sending requests to that failing service for 90 seconds. This helps your application gracefully degrade.
That is a conservative, deliberately boring design. The failure rate is evaluated over a window rather than per request, so a single timeout does not open anything. The volume threshold prevents a circuit from opening on a handful of failures during a quiet period, when a 50% rate over two calls means nothing statistically. And the sleep window is long enough that a dependency which is genuinely down is not hammered while it recovers.
The pattern reference in the README is Martin Fowler's circuit breaker article, which is the standard explanation of the closed, open and half-open states. The gem implements the pattern and the majority of the work for you: you wrap a call, and the wrapping decides whether to make it.
The audience is a Ruby application with service dependencies, and the project description says it plainly: a circuit breaker built with large Ruby apps in mind, which is a hint about where the configuration surface is aimed.
Two ways to call it, and the difference is nil versus an exception
The basic usage is one call:
Circuitbox.circuit(:your_service, exceptions: [Net::ReadTimeout]) do
Net::HTTP.get URI('http://example.com/api/messages')
endCircuitbox will return nil for failed requests and open circuits. That is the first of the two calling conventions, and it suits code that treats a dependency as best effort.
The second convention raises. The README's example wraps the call in a method on a service client and uses the run method:
class ExampleServiceClient
def circuit
Circuitbox.circuit(:yammer, exceptions: [Zephyr::FailedRequest])
end
def http_get
circuit.run(exception: false) do
Zephyr.new("http://example.com").get(200, 1000, "/api/messages")
end
end
endUsing the run method will throw an exception when the circuit is open or the underlying service fails. The exception: false option in that example suppresses the raise and falls back to the nil behaviour, so the two styles are one flag apart rather than two different mechanisms.
That choice matters more than it looks. A circuit that returns nil and a circuit that raises put the failure handling in different places: one expects the caller to check for a missing result, the other expects an exception handler upstream. A large application that mixes both styles will have a hard time reasoning about which failures degrade gracefully and which propagate.
The other thing to notice in the example is the exceptions option. If your HTTP client has its own conditions for failure, you can pass an exceptions option, and the circuit tracks only those exception classes when counting failures. A client that signals failure with a status code rather than an exception needs a different signal, and the option list is where you say which signals count.
Per-circuit thresholds, and options that can be Procs
Every threshold a circuit uses is configurable per circuit, and the README documents each one with a comment that is more useful than the parameter name.
class ExampleServiceClient
def circuit
Circuitbox.circuit(:your_service, {
# exceptions circuitbox tracks for counting failures (required)
exceptions: [YourCustomException],
# seconds the circuit stays open once it has passed the error threshold
sleep_window: 300,
# length of interval (in seconds) over which it calculates the error rate
time_window: 60,
# number of requests within `time_window` seconds before it calculates error rates (checked on failures)
volume_threshold: 10,
# the store you want to use to save the circuit state so it can
# tracked, this needs to be Moneta compatible, and support increment
circuit_store: Circuitbox::MemoryStore.new,
# exceeding this rate will open the circuit (checked on failures)
error_threshold: 50,
# Customized notifier
notifier: Notifier.new
})
end
endTwo comments carry design information. The volume threshold and the error threshold are both checked on failures, not on every request, so the counters only move when something has already gone wrong. And the sleep window is the time the circuit stays open once it has passed the error threshold.
The defaults in the example are not the library defaults, which is worth noticing: the sample raises the volume threshold to 10, the sleep window to 300 seconds and the error threshold to 50 percent, where the README's default description is more than 5 requests, 90 seconds and 50 percent.
The last feature in this section is the one that catches people out. You can also pass a Proc as an option value, which will evaluate each time the circuit breaker is used. That lets you configure the circuit breaker without having to restart the processes:
Circuitbox.circuit(:yammer, {
sleep_window: Proc.new { Configuration.get(:sleep_window) },
exceptions: [Net::ReadTimeout]
})For an application that runs a fleet of processes with a configuration service, that turns a deploy into a configuration change.
Global defaults, and the cache that has to be cleared
Circuitbox defaults can be configured through Circuitbox.configure, and there are two settings.
default_circuit_store defaults to a Circuitbox::MemoryStore, and this can be changed to a compatible Moneta store. default_notifier defaults to Circuitbox::Notifier::ActiveSupport if ActiveSupport::Notifications is defined, otherwise it defaults to Circuitbox::Notifier::Null.
Circuitbox.configure do |config|
config.default_circuit_store = Circuitbox::MemoryStore.new
config.default_notifier = Circuitbox::Notifier::Null.new
endThe ActiveSupport default is a thoughtful touch. If the application is a Rails app, circuit state changes are published on ActiveSupport::Notifications without any work from you, and if it is not, the gem stays out of the way rather than pulling in a dependency.
The next paragraph is the one to internalise. After configuring circuitbox through Circuitbox.configure, the internal circuit cache of Circuitbox.circuit is cleared. And any circuit created manually through Circuitbox::CircuitBreaker before updating the configuration will need to be recreated to pick up the new defaults.
So global configuration is not retroactive for circuits you built yourself, and the cache reset means circuits created through the Circuitbox.circuit helper are rebuilt. That is the right behaviour for a process that configures at boot, and it is a trap for a process that reconfigures at run time and expects existing circuits to change shape. If you need a value to change at run time, the Proc option from the previous section is the mechanism the gem gives you, not the global configuration.
The notification side is documented separately in docs/circuit_notifications.md, and the repository carries a docs directory with that and the 2.0 upgrade guide.
Store requirements that decide whether a circuit can be shared
The circuit store holds all the relevant data to trip the circuit if a given number of requests fail in a specified period of time. By default Circuitbox uses an in-memory store, but it also supports Moneta for alternative storage options, and to use Moneta you add it to your project dependencies.
The requirements on that store are specific, and the README lists four.
It must support increment operations, which is true for most but not all available stores. It must support key expiry. It must support bulk read operations. And it must support concurrent access if shared between processes, with the example given being KyotoCabinet, which is single-writer and multiple-readers and can cause issues when multiple circuits need write access.
That fourth point is the one that bites in production. The in-memory default works perfectly for a single process, which is the common case for a Rails application with one box of workers. The moment you run the same application on several machines and want the circuit state shared, you need a store that satisfies all four requirements, and a store that fails the concurrency requirement produces a circuit that trips inconsistently rather than one that fails loudly.
The requirements are also a reasonable filter for whether sharing is worth the complexity. If each process has its own in-memory circuit, the worst case is that each process discovers the outage separately, which is usually acceptable. Shared state is for deployments where the trip decision has to be consistent across the fleet, and for those the store requirements are the price.
The Faraday middleware, its 503 default and its limits
Circuitbox ships with a Faraday HTTP client middleware, tested against faraday versions >= 0.17 through ~> 2.0.
require 'faraday'
require 'circuitbox/faraday_middleware'
conn = Faraday.new(:url => "http://example.com") do |c|
c.use Circuitbox::FaradayMiddleware
end
response = conn.get("/api")
if response.success?
# success
else
# failure or open circuit
endBy default the Faraday middleware returns a 503 response when the circuit is open, and the original response is available on the returned object. The middleware has four configurable options, each with a code example in the README.
default_value is the value to return for open circuits, defaulting to the 503 response wrapping the original response, and it can be overwritten with either a static value or a lambda which is passed the original_response and the original_error. The original_response is populated if Faraday returned an error response, and the original_error is populated if an error was thrown before Faraday returned a response. identifier sets the circuit id, defaulting to the request url, which lets several requests to one service share a circuit or keep separate circuits per route. circuit_breaker_options initialises the circuit with defaults, accepting the same options as Circuitbox::CircuitBreaker#new. And open_circuit is a lambda determining what response is considered a failure for the purpose of counting towards opening the circuit, for example a response whose status is 500 or above.
The last option is what makes the middleware usable with a client that signals failure in the response body rather than with an exception.
There is one stated limitation, and it is a real one: the middleware does not support parallel requests through a connections in_parallel method. Any application that fans out requests concurrently through Faraday will not get the protection it expects from this path, and the README does not describe a workaround.
Release history, licence and where circuitbox fits
The project is maintained under the yammer organisation, the licence is recorded as one that GitHub cannot classify, and the LICENSE file in the repository root is the text that governs.
The release record is sparse. The recent tags are v2.0.0.pre4 on 2021-01-21, v2.0.0.pre5 on 2023-04-06, and v2.0.0 on 2023-05-04. The repository is not archived and the last push was on 2026-10-19, so the code is being worked on even though no newer tag has been cut. That is the pattern of a library whose users are on a stable major version and whose maintainers are making small corrections rather than shipping features.
For an adopter that means two things. The 2.x API is what you will get, and the upgrade guide in docs/2.0-upgrade.md is the document to read if you are coming from 1.x, because the README links it as the answer for anyone upgrading to 2.x.
On scope, circuitbox is a circuit breaker and nothing more. It does not retry, it does not queue, it does not fall back to a stale response, and it does not measure latency. It counts failures, and it stops calling. If what you need is a retry budget with backoff, a hedged request, or a bulkhead that limits concurrency to a dependency, this gem is the wrong tool, because the concept it implements is deliberately narrow.
Within that narrowness it is well designed. The nil and raise conventions give you a choice of failure semantics, the thresholds are all explicit and documented in comments, the store requirements are stated, and the Faraday integration covers the common case including the 503 response and the custom open_circuit predicate. For a large Ruby application making many outbound calls, that is the tool that the pattern calls for.
Editorial conclusion
Ruby teams whose services call flaky dependencies should use circuitbox, because the default behaviour of tracking failures in one minute intervals and opening the circuit for 90 seconds is exactly the graceful degradation pattern it was written for, and the per-circuit thresholds are explicit rather than implicit. It is the wrong choice if a single failure should be retried rather than tolerated, since the gem counts failures over a window rather than retrying a call, and it is the wrong choice if your dependency is a queue or a local call where latency matters more than availability. Before adopting, read the 2.0 upgrade notes, check that your Moneta store supports increment and expiry if you share circuits between processes, and note that the newest tagged release is v2.0.0 from 2023 while the last push was on 2026-10-19.
Frequently asked questions
How do I install circuitbox?
Add gem 'circuitbox' to your application's Gemfile and run bundle, or install it directly with gem install circuitbox. The repository carries a gemspec, a Rakefile, a .rubocop.yml, a benchmark directory and a test directory.
What happens when a circuitbox circuit opens?
Circuitbox returns nil for failed requests and open circuits when you use the circuit call, and throws an exception when the circuit is open or the underlying service fails if you use the run method. The default is to stop sending requests to the failing service for 90 seconds once more than 5 requests have been made with a 50 percent failure rate.
Can I share circuit state between processes?
Yes, by using a Moneta compatible store instead of the default Circuitbox::MemoryStore. The README requires the store to support increment operations, key expiry and bulk reads, and to support concurrent access if it is shared between processes, warning that a single-writer store such as KyotoCabinet can cause issues when multiple circuits need write access.
Does the Circuitbox Faraday middleware support parallel requests?
No. The README states that the middleware does not support parallel requests through a connections in_parallel method, and it has been tested against faraday versions greater than or equal to 0.17 through about 2.0.
Official sources
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.
[](https://hysenlabs.com/projects/yammer-circuitbox)Was this useful?