class RateLimiter
def initialize(key:, limit:, window:)
@key = "rate_limit:#{key}"
@limit = limit
@window = window
end
def allowed?
now = Time.current.to_i
window_start = now - @window
Redis.current.multi do |pipeline|
# Remove old entries
pipeline.zremrangebyscore(@key, '-inf', window_start)
# Add current request
pipeline.zadd(@key, now, "#{now}-#{SecureRandom.hex(8)}")
# Count requests in window
pipeline.zcard(@key)
# Set expiry
pipeline.expire(@key, @window + 1)
end.last <= @limit
end
def remaining
now = Time.current.to_i
window_start = now - @window
Redis.current.zremrangebyscore(@key, '-inf', window_start)
count = Redis.current.zcard(@key)
[@limit - count, 0].max
end
def reset_at
now = Time.current.to_i
window_start = now - @window
oldest = Redis.current.zrange(@key, 0, 0, with_scores: true).first
return now + @window unless oldest
oldest[1].to_i + @window
end
end
module ApiRateLimiting
extend ActiveSupport::Concern
included do
before_action :check_rate_limit
end
private
def check_rate_limit
identifier = current_user&.id || request.ip
limiter = RateLimiter.new(
key: "api:#{identifier}",
limit: 100,
window: 60
)
unless limiter.allowed?
response.set_header('X-RateLimit-Limit', '100')
response.set_header('X-RateLimit-Remaining', '0')
response.set_header('X-RateLimit-Reset', limiter.reset_at.to_s)
render json: { error: 'RATE_LIMIT_EXCEEDED' }, status: :too_many_requests
return
end
response.set_header('X-RateLimit-Limit', '100')
response.set_header('X-RateLimit-Remaining', limiter.remaining.to_s)
response.set_header('X-RateLimit-Reset', limiter.reset_at.to_s)
end
end
While Rack::Attack handles basic rate limiting, custom throttling logic gives fine-grained control over quotas, burst allowances, and per-feature limits. I implement a token bucket algorithm in Redis using sorted sets to track request timestamps per user/IP. Each request checks if the bucket has capacity and adds a timestamp; old timestamps beyond the window are pruned. This approach supports burst tolerance—users can make several requests rapidly as long as their average rate stays within limits. I expose remaining quota and reset time in response headers (X-RateLimit-Remaining, X-RateLimit-Reset) so clients can self-regulate. Different endpoints or subscription tiers get different bucket sizes.