class SlidingWindowLimiter
Result = Struct.new(:allowed, :count, :limit, :window_ms, keyword_init: true)
SCRIPT = <<~LUA.freeze
local key = KEYS[1]
local now = tonumber(ARGV[1])
local window = tonumber(ARGV[2])
local limit = tonumber(ARGV[3])
redis.call('ZREMRANGEBYSCORE', key, 0, now - window)
local count = redis.call('ZCARD', key)
if count < limit then
redis.call('ZADD', key, now, now .. '-' .. math.random(1000000))
redis.call('PEXPIRE', key, window)
return {1, count + 1}
end
return {0, count}
LUA
def initialize(redis:, limit:, window_ms:)
@redis = redis
@limit = limit
@window_ms = window_ms
@sha = @redis.script(:load, SCRIPT)
end
def allow?(client_key)
now = (Time.now.to_f * 1000).to_i
allowed, count = eval_script("ratelimit:#{client_key}", now)
Result.new(allowed: allowed == 1, count: count, limit: @limit, window_ms: @window_ms)
end
private
def eval_script(key, now)
@redis.evalsha(@sha, keys: [key], argv: [now, @window_ms, @limit])
rescue Redis::CommandError => e
raise unless e.message.include?("NOSCRIPT")
@sha = @redis.script(:load, SCRIPT)
retry
end
end
class RackAttackThrottle
def initialize(app)
@app = app
@limiter = SlidingWindowLimiter.new(
redis: Rails.application.config.redis_pool.with { |c| c },
limit: Integer(ENV.fetch("RATE_LIMIT", 100)),
window_ms: 60_000
)
end
def call(env)
request = ActionDispatch::Request.new(env)
return @app.call(env) unless request.path.start_with?("/api/")
result = @limiter.allow?(client_key(request))
env["ratelimit.result"] = result
return too_many_requests(result) unless result.allowed
@app.call(env)
end
private
def client_key(request)
token = request.get_header("HTTP_AUTHORIZATION").to_s[/Bearer\s+(.+)/, 1]
token.presence || request.ip
end
def too_many_requests(result)
retry_after = (result.window_ms / 1000.0).ceil
headers = {
"Content-Type" => "application/json",
"Retry-After" => retry_after.to_s,
"X-RateLimit-Limit" => result.limit.to_s,
"X-RateLimit-Remaining" => "0"
}
[429, headers, [{ error: "rate_limit_exceeded", retry_after: retry_after }.to_json]]
end
end
class ApiBaseController < ActionController::API
after_action :set_rate_limit_headers
private
def set_rate_limit_headers
result = request.env["ratelimit.result"]
return unless result
remaining = [result.limit - result.count, 0].max
response.set_header("X-RateLimit-Limit", result.limit.to_s)
response.set_header("X-RateLimit-Remaining", remaining.to_s)
response.set_header("X-RateLimit-Reset", reset_at(result).to_s)
end
def reset_at(result)
(Time.now + (result.window_ms / 1000.0)).to_i
end
end
Rate limiting protects an API from abusive or accidental traffic bursts, and a sliding window implementation avoids the boundary spikes that a naive fixed-window counter suffers from. The classic problem with fixed windows is that a client can send a full quota at the end of one window and another full quota at the start of the next, effectively doubling the allowed rate around the boundary. This snippet models a per-client window using a Redis sorted set (ZSET) keyed by request timestamp, which gives a true rolling window at the cost of storing one member per request.
In SlidingWindowLimiter, the entire decision is pushed into a single Lua script executed with EVALSHA. Running everything server-side in Redis is deliberate: it makes the read-count-add-expire sequence atomic, so concurrent requests from the same client cannot race between checking the count and recording their own hit. The script first calls ZREMRANGEBYSCORE to evict entries older than the window, then ZCARD to count what remains, and only if the count is under limit does it ZADD the new request and refresh the key's TTL with PEXPIRE. The TTL is set to the window length on every allowed call so idle keys expire on their own, keeping memory bounded without a separate sweeper.
The script returns both an allowed flag and the current count, letting the caller compute the standard X-RateLimit-* headers. allow? wraps this and falls back through NOSCRIPT handling, reloading the script if Redis was flushed or failed over. A key trade-off worth noting: because each request adds a member, memory scales with request volume within the window, so very high limits are better served by an approximate counter.
The RackAttackThrottle initializer wires the limiter into the request path as Rack middleware, deriving a client key from the API token or IP and short-circuiting with a 429 plus a Retry-After header when allow? returns false. Keeping the throttling logic in middleware means it runs before controllers, ActiveRecord, and view rendering, rejecting excess load as cheaply as possible. The controller in ApiBaseController reads the limiter result exposed on the Rack env to attach informational headers on successful responses, so well-behaved clients can self-pace against their remaining budget.
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
payload = {
sub: user.id,
iss: 'https://auth.example.com',
aud: 'codesnips-api',
exp: 15.minutes.from_now.to_i,
iat: Time.now.to_i,
JWT issuance and verification without common footguns
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
Share this code
Here's the card — post it anywhere.