Stoplight: a Ruby circuit breaker with a traffic light state model
:traffic_light: Traffic control for code.
At a glance
- What is it?
- Stoplight wraps unreliable calls in a circuit breaker that moves through green, red and yellow, raising a RedLight error instead of calling a dependency that is known to be failing. It works with no configuration, counts errors in a rolling window, and ships a built-in admin panel for observing and locking lights.
- Who is it for?
- Stoplight suits a Ruby service that calls flaky dependencies and needs the failure of one call to stop the cascade rather than pile up, and the built-in admin panel, including manual locking of a light, is what makes it usable in production rather than only in tests.
- 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 1 day 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 22, 2026, and from our analysis. They are not legal advice.
Editorial analysis
Three states, four parameters
Stoplight is described as a traffic control for code, an implementation of the circuit breaker pattern in Ruby, and it helps your application gracefully handle failures in external dependencies like flaky databases, unreliable APIs or spotty web services, preventing cascading failures from affecting your entire application. The state model is the circuit breaker one, spelled out. Green is normal operation, code runs as expected, and the circuit is closed. Red is the failure state, where the code fast-fails without running, and the circuit is open. Yellow is the recovery state, which allows a test execution to see if the problem is resolved, and the circuit is half-open. Two parameters govern the transition to red: the window size, defaulting to nil, which is the time window in which errors are counted toward the threshold, and by default all errors are counted, and the threshold, defaulting to 3, the number of errors required to move from green to red. Two more govern what happens afterwards: the cool off time, defaulting to 60 seconds, which is the time to wait in the red state before transitioning to yellow, and the recovery threshold, defaulting to 1, the number of successful attempts required to transition from yellow back to green.
The default behaviour needs no configuration at all
The basic usage is short enough to be the whole story for a first integration. You create a stoplight with a name and run the risky code inside it:
# Create a stoplight with default settings
light = Stoplight("Payment Service")
# Use it to wrap code that might fail
result = light.run { payment_gateway.process(order) }The README's own demonstration of the failure path is a division by zero, and the sequence is what makes the mechanism concrete. Three failing calls each raise ZeroDivisionError, and after the last failure the light turns red, so the next call raises a Stoplight::Error::RedLight without executing the block at all, and light.color returns :red. The error is not a bare exception, it carries metadata. The example shows a rescue block reading error.light_name for the name, error.cool_off_time for the configured wait, and error.retry_after, an absolute time after which a recovery attempt can occur. That last field is the one an API layer needs: it can turn a fast-fail into a proper 503 with a Retry-After header, without the caller having to know how the breaker's internals work.
The red light raises, and a fallback can absorb it
Two design choices sit side by side, and you pick per call site. The default is to raise Stoplight::Error::RedLight, which is the safe choice when the caller must know the dependency is down. The alternative is a fallback, which is a callable that receives the error:
fallback = ->(error) { error ? "Failed: #{error.message}" : "Service unavailable" }
light = Stoplight('example-fallback')
result = light.run(fallback) { external_service.call }The contract is worth reading carefully, because the nil is the interesting part. If the light is green but the call fails, the fallback receives the error. If the light is red, the fallback receives nil, because there was no error to report, only a refusal to call. In both cases the return value of the fallback becomes the return value of the run method, so the same code path can serve a degraded cached response and a hard failure without branching on the state. The example fallback shows the two messages it would produce, a failed message carrying the error text and a service unavailable message for the red state. That is the difference between a breaker that leaks exceptions and one that does not.
A built-in admin panel for watching and locking lights
Stoplight comes with an admin panel for observing and controlling all lights across your application, showing each light's current state, recent failures, and providing controls to lock and unlock lights manually. The manual lock is the feature that matters most in practice, because it lets an operator take a light out of service during a known upstream incident instead of waiting for the threshold logic. For a Rails application the panel is mounted as a Rack app with authentication, and the example uses Rack::Auth::Basic with credentials from the environment:
Rails.application.routes.draw do
Stoplight::Admin.use(Rack::Auth::Basic) do |username, password|
username == ENV["STOPLIGHT_ADMIN_USERNAME"] && password == ENV["STOPLIGHT_ADMIN_PASSWORD"]
end
mount Stoplight::Admin => '/stoplights'
endThe credentials come from environment variables:
export STOPLIGHT_ADMIN_USERNAME=admin
export STOPLIGHT_ADMIN_PASSWORD=secretAnd the panel needs two gems the README flags as important, sinatra and sinatra-contrib, both with require set to false:
gem "sinatra", require: false
gem "sinatra-contrib", require: falseThe panel can also run as a standalone service in Docker, with the image bolshakov/stoplight-admin, taking a REDIS_URL, the two credential variables, and publishing port 4567:
docker run \
-e REDIS_URL=redis://localhost:6379 \
-e STOPLIGHT_ADMIN_USERNAME=admin \
-e STOPLIGHT_ADMIN_PASSWORD=secret \
-p 4567:4567 \
bolshakov/stoplight-adminThat standalone mode is the one to note, because the REDIS_URL is where the distributed behaviour comes from.
Global defaults and per-light overrides
Stoplight allows you to set default values for all lights in your application, and the configuration block shows the full vocabulary, including two settings the earlier sections did not mention. The traffic_control setting chooses the policy, and the global example sets it to :error_rate with a window_size of 300, a threshold of 0.5, a cool_off_time of 30 and a recovery_threshold of 5, which is a materially different policy from the three-errors-in-a-row default, since a fractional threshold over a five-minute window trips on a rate rather than a count. The example also sets a default data store and notifiers, a Stoplight::DataStore::Redis.new and a Stoplight::Notifier::Logger writing to Rails.logger, and the error handling defaults, tracked_errors set to StandardError and CustomError and skipped_errors set to ActiveRecord::RecordNotFound. Per-light settings override the global ones, and the creation example shows all of them at once:
light = Stoplight("Payment Service",
window_size: 300, # Only count errors in the last five minutes
threshold: 5, # 5 errors before turning red
cool_off_time: 60, # Wait 60 seconds before attempting recovery
recovery_threshold: 1, # 1 successful attempt to turn green again
tracked_errors: [TimeoutError], # Only count TimeoutError
skipped_errors: [ValidationError] # Ignore ValidationError
)The error filtering is the part that prevents the obvious false positive. A validator raising on bad input is not a sign that the dependency is broken, and skipped_errors is how you say so.
What the data store decides about your topology
Here is the constraint that decides whether Stoplight is a local safety net or a distributed one. The default data store is local to the process, so a light named Payment Service is a different light in every worker, every thread and every replica. Under a threaded or multi-process server, that means the threshold is effectively per process, so a service with sixteen workers needs sixteen times the configured errors before any individual worker trips. The Redis data store exists to fix exactly this, by holding the light state somewhere shared, and the standalone admin panel image requires a REDIS_URL, which is why that image is only coherent in a setup where the state is shared. The alternative approach, and the one to weigh before adopting, is to put the breaker in the component that owns the call rather than in each caller, so that a single object decides and the state lives in one place per request path. Either way, the choice of data store is the choice between a breaker that is per-process and one that is per-service, and the README's default quietly picks the first.
Version 6, a 5.x branch, and the licence
The README carries a warning that you are currently browsing the documentation for Stoplight 6, and that the documentation of the previous version 5.x is on a separate branch, at the tag v5.8.3. The release history shows a long 5.x line and then a 6.0.0 with several release candidates before it, with v6.0.0.rc5 on 2026-09-28 and v6.0.0 itself on 2026-10-02, and the last push to the repository on 2026-10-22. That candidate sequence is worth noting for anyone planning an upgrade, because a major version in Ruby with a run of release candidates usually means interface changes, and the repository carries an UPGRADING.md file alongside CHANGELOG.md and the SemVer promise the README mentions. The licence is MIT, held in a LICENSE.md file at the root. The repository layout also shows how the project is developed: RSpec configuration in.rspec, SimpleCov in.simplecov, rubocop in its own directory with a.stoplight_standard.yml for the project-specific rules, a Steepfile with rbs_collection files for type checking, cucumber.yml with a features directory for integration tests, a bench directory, and a config.ru for the rack application.
Editorial conclusion
Stoplight suits a Ruby service that calls flaky dependencies and needs the failure of one call to stop the cascade rather than pile up, and the built-in admin panel, including manual locking of a light, is what makes it usable in production rather than only in tests. It is the wrong tool if you need a distributed breaker, because the default configuration is a local in-process data store and the Redis store is opt-in, so two processes do not share a light unless you configure them to. It is also the wrong tool if you need per-error semantics beyond tracked and skipped exceptions. Verify first which data store you are pointing at, since a local store means a light per process rather than a light per service, and note that version 6 was released on 2026-10-02 with the last push on 2026-10-22, and that the documentation you are reading is for version 6 while the 5.x documentation lives on a separate branch.
Frequently asked questions
How do I install Stoplight?
Add gem 'stoplight' to your Gemfile, or install it manually with gem install stoplight. The project uses Semantic Versioning and publishes a changelog with a detailed list of changes.
What are the default thresholds in Stoplight?
The window size defaults to nil so all errors are counted, the threshold defaults to 3 errors to turn the light red, the cool off time defaults to 60 seconds before moving to yellow, and the recovery threshold defaults to 1 successful attempt to return to green.
How does a fallback receive its argument?
If the light is green and the call fails, the fallback receives the error, and if the light is red the fallback receives nil, because the call was never made. In both cases the fallback's return value becomes the return value of the run method.
What does the Stoplight admin panel need?
The panel requires the sinatra and sinatra-contrib gems, and it needs credentials. Mounted in Rails it is wrapped in Rack::Auth::Basic reading STOPLIGHT_ADMIN_USERNAME and STOPLIGHT_ADMIN_PASSWORD, or run standalone from the bolshakov/stoplight-admin image with a REDIS_URL and those two variables.
Which errors does Stoplight count by default?
By default Stoplight tracks all StandardError exceptions. The configuration lets you set tracked_errors and skipped_errors, and the README's example skips ActiveRecord::RecordNotFound globally and tracks a narrower list such as TimeoutError on a single light.
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/bolshakov-stoplight)Was this useful?