class CircuitBreaker
class CircuitOpenError < StandardError; end
def initialize(name, redis: Redis.current, failure_threshold: 5, failure_window: 60, cooldown: 30)
@key = "circuit:#{name}"
@redis = redis
@failure_threshold = failure_threshold
@failure_window = failure_window
@cooldown = cooldown
end
def run
if open?
raise CircuitOpenError, "circuit #{@key} is open"
end
result = yield
record_success
result
rescue CircuitOpenError
raise
rescue StandardError => e
record_failure
raise e
end
def open?
opened_at = @redis.get("#{@key}:opened_at")
return false if opened_at.nil?
Time.now.to_i - opened_at.to_i < @cooldown
end
private
def record_success
@redis.del("#{@key}:failures", "#{@key}:opened_at")
end
def record_failure
failures = @redis.incr("#{@key}:failures")
@redis.expire("#{@key}:failures", @failure_window) if failures == 1
trip! if failures >= @failure_threshold
end
def trip!
@redis.set("#{@key}:opened_at", Time.now.to_i)
end
end
require "faraday"
class PaymentsGateway
class GatewayError < StandardError; end
def initialize
@breaker = CircuitBreaker.new("payments", failure_threshold: 4, cooldown: 20)
@conn = Faraday.new(url: ENV.fetch("PAYMENTS_URL")) do |f|
f.options.open_timeout = 2
f.options.timeout = 5
f.request :json
f.response :json
end
end
def charge(amount_cents:, token:)
@breaker.run do
resp = @conn.post("/v1/charges") do |req|
req.headers["Authorization"] = "Bearer #{ENV['PAYMENTS_KEY']}"
req.body = { amount: amount_cents, source: token }
end
if resp.status >= 500
raise GatewayError, "upstream #{resp.status}"
end
resp.body
end
end
end
class PaymentsController < ApplicationController
def create
result = gateway.charge(
amount_cents: charge_params[:amount_cents],
token: charge_params[:token]
)
render json: { charge_id: result["id"], status: "ok" }, status: :created
rescue CircuitBreaker::CircuitOpenError
response.headers["Retry-After"] = "20"
render json: { error: "payments temporarily unavailable" }, status: :service_unavailable
rescue PaymentsGateway::GatewayError => e
Rails.logger.warn("payment failed: #{e.message}")
render json: { error: "payment could not be processed" }, status: :bad_gateway
end
private
def gateway
@gateway ||= PaymentsGateway.new
end
def charge_params
params.require(:payment).permit(:amount_cents, :token)
end
end
A circuit breaker guards calls to an unreliable downstream service so that repeated failures stop being retried blindly. Instead of hammering a dead dependency and piling up slow, timing-out requests, the breaker "trips" after a threshold of failures and short-circuits subsequent calls for a cooldown window, failing fast until the service looks healthy again. This snippet shows a small, Redis-backed breaker wired into a Faraday client and consumed from a Rails controller.
In CircuitBreaker, the breaker models three states: :closed (normal), :open (tripped, rejecting calls), and :half_open (a trial period after cooldown). State is kept in Redis under a namespaced key so it is shared across every process and dyno, which matters because a per-instance in-memory counter would let each worker independently rediscover the outage. The run method checks open? first and raises CircuitOpenError immediately when the breaker is open and the cooldown has not elapsed. When the cooldown passes, open? returns false so exactly the next call is allowed through as a probe.
Success and failure are recorded via record_success and record_failure. A success clears the failure counter and closes the circuit; a failure increments a counter with an expiry equal to the failure window, and once it reaches failure_threshold the breaker writes an opened_at timestamp to trip. Using INCR with an EXPIRE gives a rolling window cheaply without storing individual timestamps.
PaymentsGateway composes the breaker with a real HTTP client. It configures Faraday with explicit open_timeout and timeout values — critical, since a breaker is useless if calls hang forever — and treats both network errors and 5xx responses as failures worth counting. Note that a CircuitOpenError propagates out untouched so callers can distinguish "we didn't even try" from a genuine gateway error.
PaymentsController shows the payoff: it rescues CircuitBreaker::CircuitOpenError and returns 503 with a Retry-After header, degrading gracefully instead of leaking timeouts to the user. The main trade-off is tuning: too low a threshold trips on transient blips, too long a cooldown delays recovery, and the half-open probe risks one extra failed call. This pattern fits any synchronous dependency whose latency or availability can drag down the caller.
Related snips
class CommentsController < ApplicationController
before_action :set_post
def create
@comment = @post.comments.build(comment_params)
System test: asserting Turbo Stream responses
class Post < ApplicationRecord
belongs_to :author, class_name: 'User'
has_many :comments, dependent: :destroy
scope :published, -> { where.not(published_at: nil).where('published_at <= ?', Time.current) }
scope :draft, -> { where(published_at: nil) }
ActiveRecord scopes for reusable query logic
export interface RetryOptions {
retries: number;
baseMs: number;
maxMs: number;
signal?: AbortSignal;
onRetry?: (attempt: number, delay: number, err: unknown) => void;
Exponential backoff with jitter for retries
module Api
module V1
class UsersController < BaseController
def show
user = User.includes(:profile).find(params[:id])
ETags for conditional requests and caching
class PostsController < ApplicationController
def index
@posts = Post.includes(:author)
.order(created_at: :desc)
.page(params[:page])
.per(10)
Turbo Frames: infinite scroll with lazy-loading frame
package dbutil
import (
"context"
"github.com/jackc/pgconn"
Retry Postgres serialization failures with bounded attempts
Share this code
Here's the card — post it anywhere.