Stoplight
:traffic_light: Traffic control for code.
Install / Use
/learn @bolshakov/StoplightREADME
[Stoplight][]
[![Version badge][]][version] [![Build badge][]][build] [![Coverage badge][]][coverage]
Stoplight is a traffic control for code. It's an implementation of the circuit breaker pattern in Ruby.
:warning:️ You're currently browsing the documentation for Stoplight 5.x. If you're looking for the documentation of the previous version 4.x, you can find it here.
Stoplight helps your application gracefully handle failures in external dependencies (like flaky databases, unreliable APIs, or spotty web services). By wrapping these unreliable calls, Stoplight prevents cascading failures from affecting your entire application.
The best part? Stoplight works with zero configuration out of the box, while offering deep customization when you need it.
Installation
Add it to your Gemfile:
gem 'stoplight'
Or install it manually:
$ gem install stoplight
Stoplight uses [Semantic Versioning][]. Check out [the change log][] for a detailed list of changes.
Core Concepts
Stoplight operates like a traffic light with three states:
stateDiagram
Green --> Red: Errors reach threshold
Red --> Yellow: After cool_off_time
Yellow --> Green: Successful recovery
Yellow --> Red: Failed recovery
Green --> Green: Success
classDef greenState fill:#28a745,stroke:#1e7e34,stroke-width:2px,color:#fff
classDef redState fill:#dc3545,stroke:#c82333,stroke-width:2px,color:#fff
classDef yellowState fill:#ffc107,stroke:#e0a800,stroke-width:2px,color:#000
class Green greenState
class Red redState
class Yellow yellowState
- Green: Normal operation. Code runs as expected. (Circuit closed)
- Red: Failure state. Fast-fails without running the code. (Circuit open)
- Yellow: Recovery state. Allows a test execution to see if the problem is resolved. (Circuit half-open)
Stoplight's behavior is controlled by two main parameters:
- Window Size (default:
nil): Time window in which errors are counted toward the threshold. By default, all errors are counted. - Threshold (default:
3): Number of errors required to transition from green to red.
Additionally, two other parameters control how Stoplight behaves after it turns red:
- Cool Off Time (default:
60seconds): Time to wait in the red state before transitioning to yellow. - Recovery Threshold (default:
1): Number of successful attempts required to transition from yellow back to green.
Basic Usage
Stoplight works right out of the box with sensible defaults:
# 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) }
When everything works, the light stays green and your code runs normally. If the code fails repeatedly, the
light turns red and raises a Stoplight::Error::RedLight exception to prevent further calls.
light = Stoplight("Example")
light.run { 1 / 0 } #=> raises ZeroDivisionError: divided by 0
light.run { 1 / 0 } #=> raises ZeroDivisionError: divided by 0
light.run { 1 / 0 } #=> raises ZeroDivisionError: divided by 0
After the last failure, the light turns red. The next call will raise a Stoplight::Error::RedLight exception without
executing the block:
light.run { 1 / 0 } #=> raises Stoplight::Error::RedLight: example-zero
light.color # => "red"
The Stoplight::Error::RedLight provides metadata about the error:
def run_request
light = Stoplight("Example", cool_off_time: 10)
light.run { 1 / 0 } #=> raises Stoplight::Error::RedLight
rescue Stoplight::Error::RedLight => error
puts error.light_name #=> "Example"
puts error.cool_off_time #=> 10
puts error.retry_after #=> Absolute Time after which a recovery attempt can occur (e.g., "2025-10-21 15:39:50.672414 +0600")
end
After one minute, the light transitions to yellow, allowing a test execution:
# Wait for the cool-off time
sleep 60
light.run { 1 / 1 } #=> 1
If the test probe succeeds, the light turns green again. If it fails, the light turns red again.
light.color #=> "green"
Using Fallbacks
Provide fallbacks to gracefully handle errors:
fallback = ->(error) { error ? "Failed: #{error.message}" : "Service unavailable" }
light = Stoplight('example-fallback')
result = light.run(fallback) { external_service.call }
If the light is green but the call fails, the fallback receives the error. If the light is red, the fallback
receives nil. In both cases, the return value of the fallback becomes the return value of the run method.
Admin Panel
Stoplight comes with a built-in Admin Panel that can track all active Lights and manually lock them in the desired state (Green or Red). Locking lights in certain states might be helpful in scenarios like E2E testing.

To add Admin Panel protected by basic authentication to your Rails project, add this configuration to your config/routes.rb file.
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'
# ...
end
Then set up STOPLIGHT_ADMIN_USERNAME and STOPLIGHT_ADMIN_PASSWORD env variables to access your Admin panel.
IMPORTANT: Stoplight Admin Panel requires you to have sinatra and sinatra-contrib gems installed. You can either add them to your Gemfile:
gem "sinatra", require: false
gem "sinatra-contrib", require: false
Or install it manually:
gem install sinatra
gem install sinatra-contrib
Standalone Admin Panel Setup
It is possible to run the Admin Panel separately from your application using the stoplight-admin:<release-version> docker image.
docker run --net=host bolshakov/stoplight-admin
IMPORTANT: Standalone Admin Panel should use the same Redis your application uses. To achieve this, set the REDIS_URL ENV variable via -e REDIS_URL=<url-to-your-redis-servier>. E.g.:
docker run -e REDIS_URL=redis://localhost:6378 --net=host bolshakov/stoplight-admin
Configuration
Global Configuration
Stoplight allows you to set default values for all lights in your application:
Stoplight.configure do |config|
# Set default behavior for all stoplights
config.traffic_control = :error_rate
config.window_size = 300
config.threshold = 0.5
config.cool_off_time = 30
config.recovery_threshold = 5
# Set up default data store and notifiers
config.data_store = Stoplight::DataStore::Redis.new(redis)
config.notifiers = [Stoplight::Notifier::Logger.new(Rails.logger)]
# Configure error handling defaults
config.tracked_errors = [StandardError, CustomError]
config.skipped_errors = [ActiveRecord::RecordNotFound]
end
Creating Stoplights
The simplest way to create a stoplight is with a name:
light = Stoplight("Payment Service")
You can also provide settings during creation:
data_store = Stoplight::DataStore::Redis.new(Redis.new)
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
data_store: data_store, # Use Redis for persistence
tracked_errors: [TimeoutError], # Only count TimeoutError
skipped_errors: [ValidationError] # Ignore ValidationError
)
Modifying Stoplights
You can create specialized versions of existing stoplights:
# Base configuration for API calls
base_api = Stoplight("Service API")
# Create specialized version for the users endpoint
users_api = base_api.with(
tracked_errors: [TimeoutError] # Only track timeouts
)
The #with method creates a new stoplight instance without modifying the original, making it ideal for creating
specialized stoplights from a common configuration.
Error Handling
By default, Stoplight tracks all StandardError exceptions.
Note: System-level exceptions (e.g., NoMemoryError, SignalException) are not tracked, as they are not subclasses of StandardError.
Custom Error Configuration
Control which errors affect your stoplight state. Skip specific errors (will not count toward failure threshold)
light = Stoplight("Example API", skipped_errors: [ActiveRecord::RecordNotFound, ValidationError])
Only track specific errors (only these count toward failure threshold)
light = Stoplight("Example API", tracked_errors: [NetworkError, Timeout::Error])
When both methods are used, skipped_errors takes precedence over tracked_errors.
Advanced Configuration
Traffic Control Strategies
You've seen how Stoplight transitions from green to red when errors reach the threshold. But how exactly does it decide when that threshold is reached? That's where traffic control strategies come in.
Stoplight offers two built-in strategies for counting errors:
Consecutive Errors (Default)
Stops traffic when a specified number of consecutive errors occur. Works with or without time sliding windows.
light = Stoplight(
"Payment API",
traffic_control: :consecutive_errors,
threshold: 5,
)
Counts consecutive errors regardless of when they occurred. Once 5 consecutive errors happen, the stoplight turns red and stops traffic.
light = Stoplight(
"Payment API",
traffic_control: :consecutive_errors,
threshold: 5,
window_size: 300,
)
Counts consecutive errors within a 5-minute sl
Related Skills
node-connect
347.2kDiagnose OpenClaw node connection and pairing failures for Android, iOS, and macOS companion apps
frontend-design
108.0kCreate distinctive, production-grade frontend interfaces with high design quality. Use this skill when the user asks to build web components, pages, or applications. Generates creative, polished code that avoids generic AI aesthetics.
openai-whisper-api
347.2kTranscribe audio via OpenAI Audio Transcriptions API (Whisper).
qqbot-media
347.2kQQBot 富媒体收发能力。使用 <qqmedia> 标签,系统根据文件扩展名自动识别类型(图片/语音/视频/文件)。
