philiprehberger-rate_limiter
In-memory rate limiter with sliding window and token bucket algorithms, per-key tracking, and thread safety.
Note: This is a single-process, in-memory rate limiter. It does not share state across processes or servers. For distributed rate limiting, use a centralized store like Redis.
Requirements
- Ruby >= 3.1
Installation
Add to your Gemfile:
gem "philiprehberger-rate_limiter"Then run:
bundle installOr install directly:
gem install philiprehberger-rate_limiterUsage
require "philiprehberger/rate_limiter"Sliding Window
Limits the number of requests within a rolling time window.
limiter = Philiprehberger::RateLimiter.sliding_window(limit: 100, window: 60)
if limiter.allow?("user:123")
# Request is allowed
else
# Rate limit exceeded
endToken Bucket
Allows bursts up to a capacity, refilling at a steady rate.
limiter = Philiprehberger::RateLimiter.token_bucket(rate: 10, capacity: 50)
if limiter.allow?("api:key")
# Request is allowed
else
# Rate limit exceeded
endPeeking Without Consuming
limiter.peek("user:123") # => true/false (does not consume)
limiter.remaining("user:123") # => number of remaining requests/tokensWeighted Requests
Consume multiple tokens per request for expensive operations:
limiter.allow?("user:123", weight: 5) # consumes 5 tokens
limiter.allow?("user:123", weight: 1) # consumes 1 token (default)Inspecting Usage
info = limiter.info("user:123")
# Sliding window:
# => { remaining: 98, reset_at: 1710000060.5, limit: 100, window: 60, used: 2 }
# Token bucket:
# => { remaining: 48, reset_at: 1710000000.2, capacity: 50, rate: 10.0, tokens: 48.3 }The reset_at value is a monotonic timestamp suitable for computing X-RateLimit-Reset headers. It is nil when the key has no usage or is at full capacity.
Per-Key Stats
Track allowed and rejected request counts:
limiter.stats("user:123")
# => { allowed: 42, rejected: 3 }Quota Refund
Return tokens when a downstream operation fails (so the failed request does not count):
if limiter.allow?("user:123")
begin
make_api_call
rescue ApiError
limiter.refund("user:123", amount: 1)
end
endOn-Reject Callback
Register a hook for logging or alerting when requests are rejected:
limiter.on_reject do |key|
logger.warn("Rate limit exceeded for #{key}")
endThe method returns self for chaining:
limiter = Philiprehberger::RateLimiter
.sliding_window(limit: 100, window: 60)
.on_reject { |key| logger.warn("Rejected: #{key}") }Resetting a Key
limiter.reset("user:123")Sliding Window vs Token Bucket
| Feature | SlidingWindow | TokenBucket |
|---|---|---|
| Best for | Fixed request counts per window | Allowing bursts with steady refill |
| Parameters |
limit, window (seconds) |
rate (tokens/sec), capacity
|
| Burst behavior | No bursting beyond limit | Allows bursts up to capacity |
| Memory | Stores timestamps per request | Stores one float + timestamp per key |
API
| Method | Description |
|---|---|
RateLimiter.sliding_window(limit:, window:) |
Create a sliding window limiter |
RateLimiter.token_bucket(rate:, capacity:) |
Create a token bucket limiter |
#allow?(key, weight: 1) |
Check and consume token(s); returns true/false
|
#peek(key) |
Check availability without consuming |
#remaining(key) |
Return remaining request/token count |
#reset(key) |
Clear all state for a key |
#info(key) |
Return usage info hash (remaining, reset_at, limit/capacity, used/tokens) |
#stats(key) |
Return { allowed:, rejected: } counters for a key |
#refund(key, amount: 1) |
Return tokens/slots on error |
#on_reject { |key| } |
Register a callback for rejected requests |
SlidingWindow#limit |
Return the configured request limit |
SlidingWindow#window |
Return the configured window duration (seconds) |
TokenBucket#rate |
Return the configured refill rate (tokens/sec) |
TokenBucket#capacity |
Return the configured token capacity |
Development
bundle install
bundle exec rspec # Run tests
bundle exec rubocop # Check code styleLicense
MIT