philiprehberger-rate_limiter

Tests Gem Version Last updated

In-memory rate limiter with sliding window and token bucket

Requirements

  • Ruby >= 3.1

Installation

Add to your Gemfile:

gem "philiprehberger-rate_limiter"

Or install directly:

gem install philiprehberger-rate_limiter

Usage

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
end

Token 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
end

No-op Limiter

A limiter that always allows requests — useful in test environments or when a feature is behind a kill-switch.

limiter = Philiprehberger::RateLimiter.noop
limiter.allow?("anyone")    # => true
limiter.remaining("anyone") # => Float::INFINITY

Peeking Without Consuming

limiter.peek("user:123")      # => true/false (does not consume)
limiter.remaining("user:123") # => number of remaining requests/tokens

Weighted 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
end

On-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}")
end

The method returns self for chaining:

limiter = Philiprehberger::RateLimiter
  .sliding_window(limit: 100, window: 60)
  .on_reject { |key| logger.warn("Rejected: #{key}") }

Throttle (Execute if Allowed)

result = limiter.throttle("user:123") { make_api_call }
result[:allowed]  # => true
result[:value]    # => the return value of make_api_call

# When rejected:
result = limiter.throttle("user:123") { make_api_call }
result[:allowed]  # => false
result[:value]    # => nil

Allow! (Raise on Rejection)

limiter.allow!("user:123")  # => true, or raises RateLimitExceeded

Listing Tracked Keys

limiter.keys  # => ["user:123", "user:456"]

Wait Time

Check how long until the next request is allowed:

limiter = Philiprehberger::RateLimiter.sliding_window(limit: 100, window: 60)
limiter.wait_time  # => 0.0 (allowed now)

# After hitting the limit:
limiter.wait_time  # => 12.5 (seconds to wait)

Window Reset

limiter.window_reset_at  # => 2026-04-01 12:01:00 +0000 (Time when window expires)

Resetting a Key

limiter.reset("user:123")  # clear state for one key
limiter.clear              # clear state for all keys

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
RateLimiter.noop Create a limiter that always allows requests
#allow?(key, weight: 1) Check and consume token(s); returns true/false
#allow!(key, weight: 1) Like allow? but raises RateLimitExceeded on rejection
#throttle(key, weight: 1) { } Execute block if allowed; returns { allowed:, value: }
#peek(key) Check availability without consuming
#remaining(key) Return remaining request/token count
#reset(key) Clear all state for a key
#clear Clear all state for every tracked key
#keys Return all currently tracked keys
#info(key) Return usage info hash (remaining, reset_at, limit/capacity, used/tokens)
#stats(key) Return { allowed:, rejected: } counters for a key
#wait_time(key) Seconds until next request is allowed (0 if now). TokenBucket also accepts weight: keyword argument
SlidingWindow#window_reset_at(key) Time when current window expires
#refund(key, amount: 1) Return tokens/slots on error
`#on_reject { \ key\
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
bundle exec rubocop

Support

If you find this project useful:

Star the repo

🐛 Report issues

💡 Suggest features

❤️ Sponsor development

🌐 All Open Source Projects

💻 GitHub Profile

🔗 LinkedIn Profile

License

MIT