Baike.dev
All toolsAI codingTrendingOpen sourceNewsSubmit
Log in
< Back to tools
S

stoplight

> DevOps
Open source

:traffic_light: Traffic control for code.

624 stars0 likes2 views
WebsiteGitHub

About

:traffic_light: Traffic control for code.

[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 6. If you're looking for the documentation of the previous version 5.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:

![Stoplight state diagram][]

  • 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 for observing and controlling all lights across your application. It displays each light's current state, recent failures, and provides controls to lock/unlock lights manually.

Basic Setup

Add the Admin Panel to your Rails application with authentication:

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 environment variables:

export STOPLIGHT_ADMIN_USERNAME=admin
export STOPLIGHT_ADMIN_PASSWORD=secret

IMPORTANT: Stoplight Admin Panel requires sinatra and sinatra-contrib gems:

gem "sinatra", require: false
gem "sinatra-contrib", require: false

Standalone Docker Setup

Run the Admin Panel as a separate service:

docker run \
  -e REDIS_URL=redis://localhost:6379 \
  -e STOPLIGHT_ADMIN_USERNAME=admin \
  -e STOPLIGHT_ADMIN_PASSWORD=secret \
  -p 4567:4567 \
  bolshakov/stoplight-admin

For complete setup and multi-system configuration details, see the Admin Panel guide.

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:

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
)

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.

Either list can be replaced for a single call without changing the light's configuration:

light.run(tracked_errors: [Timeout::Error]) { fetch_data }
light.run(skipped_errors: [ValidationError]) { process_data }

Any list omitted from run keeps its configured value. The provided list is replaced only for that call, and skipped_errors still takes precedence over tracked_errors.

Advanced Configuration

Registering Lights

Calling Stoplight("name", ...) at every call site works well for a handful of lights. As an app grows, repeating the same settings everywhere makes them easy to drift out of sync, and there's no single place listing what lights exist.

Register a light once and look it up by name wherever you need it, instead of repeating the same settings at every call site.

# config/initializers/stoplight.rb
Stoplight.register("Payment Service", threshold: 5, cool_off_time: 60)
# anywhere else in your app
Stoplight.light("Payment Service").run { payment_gateway.process(order) }

Stoplight("name", ...) still works as shown above -- registration is an addition, not a replacement. Stoplight.light is also approximately 10 times faster, since it's a plain lookup rather than re-validating the configuration on every call.

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 sliding window. Both conditions must be met: 5 consecutive errors AND at least 5 total errors within the window.

This is Stoplight's default strategy when no traffic_control is specified. You can omit traffic_control parameter in the above examples:

light = Stoplight(
  "Payment API",
  threshold: 5,
)

Error Rate

Stops traffic when the error rate exceeds a percentage within a sliding time window. Requires window_size to be configured:

light = Stoplight(
  "Payment API", 
  traffic_control: :error_rate, 
  window_size: 300, 
  threshold: 0.5,
)

Monitors error rate over a 5-minute sliding window. The stoplight turns red when error rate exceeds 50%.

Error rate evaluation starts only after 100 requests within the window — enough samples for a statistically reliable estimate. If your service handles fewer than 100 requests per window, the breaker will never trip on error rate; use traffic_control: :consecutive_errors instead.

When to use:

  • Consecutive Errors: Low-medium traffic, simple behavior, occasional spikes expected
  • Error Rate: High traffic, percentage-based SLAs, variable traffic patterns

Traffic Recovery Strategies

In the yellow state, Stoplight behaves differently from normal (green) operation. Instead of blocking all traffic, it allows a limited number of real requests to pass through to the underlying service to determine if it has recovered. These aren't synthetic probes - they're actual user requests that will execute normally if the service is healthy.

After collecting the necessary data from these requests, Stoplight decides whether to return to green or red state.

Traffic Recovery strategies control how Stoplight evaluates these requests during the recovery phase.

Consecutive Successes (Default)

Returns to green after a specified number of consecutive successful recovery attempts. This is the default behavior.

light = Stoplight(
  "Payment API", 
  traffic_recovery: :consecutive_successes, 
  recovery_threshold: 3,
)

This configuration requires 3 consecutive successful recovery probes before resuming normal traffic. If any probe fails during recovery, the stoplight immediately returns to red and waits for another cool-

Issues· 0 open

View all issuesOpen on GitHub

No open issues yet, or sync has not completed.

> Tags

Rubycircuit-breakerdistributed-systemsfault-tolerancemicroservices

No comments yet. Be the first to share.

> Details

PublishedAug 1, 2026
UpdatedSep 17, 2026
CategoryDevOps
PricingOpen source

> Related tools

D
Docker
容器化平台,标准化应用交付
G
GitHub Actions
GitHub 原生 CI/CD 工作流
N
Nginx
高性能 Web 服务器与反向代理