SkillAgentSearch skills...

Stoplight

:traffic_light: Traffic control for code.

Install / Use

/learn @bolshakov/Stoplight
About this skill

Quality Score

0/100

Supported Platforms

Universal

README

[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:

  1. Window Size (default: nil): Time window in which errors are counted toward the threshold. By default, all errors are counted.
  2. 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:

  1. Cool Off Time (default: 60 seconds): Time to wait in the red state before transitioning to yellow.
  2. 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.

Admin Panel Screenshot

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

View on GitHub
GitHub Stars588
CategoryDevelopment
Updated1d ago
Forks50

Languages

Ruby

Security Score

100/100

Audited on Apr 2, 2026

No findings