ruby lua 115 lines · 4 tabs

Sliding-Window API Rate Limiting with Rack Middleware and Redis

Shared by codesnips Jul 2026
4 tabs
require "securerandom"
require "digest"

class SlidingWindowLimiter
  Result = Struct.new(:allowed, :count, :remaining)

  def initialize(redis:, limit:, window_ms:)
    @redis = redis
    @limit = limit
    @window_ms = window_ms
    @script = File.read(File.expand_path("rate_limit.lua", __dir__))
    @sha = Digest::SHA1.hexdigest(@script)
  end

  def allow?(client_key)
    now_ms = redis_time_ms
    member = "#{now_ms}-#{SecureRandom.hex(4)}"
    allowed, count = eval_script(client_key, now_ms, member)
    Result.new(allowed == 1, count, [@limit - count, 0].max)
  end

  private

  def eval_script(key, now_ms, member)
    args = [now_ms, @window_ms, @limit, member]
    @redis.evalsha(@sha, keys: ["ratelimit:#{key}"], argv: args)
  rescue Redis::CommandError => e
    raise unless e.message.include?("NOSCRIPT")
    @redis.eval(@script, keys: ["ratelimit:#{key}"], argv: args)
  end

  def redis_time_ms
    secs, micros = @redis.time
    (secs * 1000) + (micros / 1000)
  end
end
4 files · ruby, lua Explain with highlit

Rate limiting protects an API from bursts and abuse by capping how many requests a client may make in a rolling time span. This snippet implements a sliding-window limiter as Rack middleware backed by Redis, using a sorted set per client so the window slides continuously rather than resetting on fixed boundaries. Fixed-window counters are simpler but suffer from the boundary problem: a client can send a full quota at the end of one window and another full quota at the start of the next, briefly doubling the intended rate. A sliding window avoids that by scoring every request with its timestamp and only counting those inside the trailing window.

The SlidingWindowLimiter file holds the core algorithm. Each client gets a Redis sorted set keyed by identity; request timestamps (in milliseconds) are the members and the scores. On every call, #allow? removes entries older than now - window with ZREMRANGEBYSCORE, counts the survivors with ZCARD, and, if under the limit, adds the new timestamp and sets a TTL so idle keys expire. Running these steps individually would be racy under concurrency, so they are executed atomically inside a Lua script via EVALSHA. The script returns both the current count and the allowance decision, which lets the middleware emit accurate X-RateLimit-Remaining headers without a second round trip.

The Lua body lives in rate_limit.lua. Because Redis runs the whole script as a single atomic operation, no other client can interleave between the prune, the count, and the insert. KEYS[1] is the client bucket; the ARGV carry the current time, window size, limit, and a unique member value (timestamp plus a random suffix) so two requests landing in the same millisecond do not collide in the sorted set. The script returns {allowed, count}, and it only writes the new member when the request is actually permitted, keeping rejected requests from inflating the window.

The RackThrottle middleware wires this into a Rack app. #call derives a client key — an API token when present, otherwise the remote IP — and consults the limiter. When the request is allowed it forwards to the next app in the stack and decorates the response with X-RateLimit-Limit and X-RateLimit-Remaining. When the limit is exceeded it short-circuits with a 429 Too Many Requests JSON body and a Retry-After header, never touching the downstream app. Fail-open behavior is deliberate: if Redis raises, #call logs and lets the request through, since a limiter outage should not become a full outage.

The config.ru file shows the assembly, inserting the middleware ahead of the application with a shared Redis pool and per-route policy. Trade-offs worth noting: sorted sets cost memory proportional to requests-in-window per client, so very high limits should prefer approximate counters; and clock skew across app servers is avoided by using Redis TIME as the authority rather than each host's clock.

Share this code

Here's the card — post it anywhere.

Sliding-Window API Rate Limiting with Rack Middleware and Redis — share card
Link copied