Rate Limit Challenge



itMcsy5.gif
 
I needed a Redis-backed rate-limiter so I could scale to more than one dyno, though I took some middleware ideas from another implementation.

Gist: https://gist.github.com/danneu/1d4c2f6a8e47935dbfb0

It just uses a Redis hashmap "ratelimits" where each field key/val pair is ip/hit-count. It also stores the current window in "ratelimits:window" which would be a bucketed value 1-96 if your window is 15min (96x 15min intervals in a day). The current window is a function of window-duration and seconds-since-midnight. If the window-duration is 15min, then time 00:00:00 is in bucket 1 and time 23:59:59 is in bucket 96.

Code:
{:status 200, :body "Hello world", :headers {"X-Rate-Limit-Reset" "735", "X-Rate-Limit-Remaining" "4", "X-Rate-Limit-Limit" "5"}}
{:status 200, :body "Hello world", :headers {"X-Rate-Limit-Reset" "732", "X-Rate-Limit-Remaining" "3", "X-Rate-Limit-Limit" "5"}}
{:status 200, :body "Hello world", :headers {"X-Rate-Limit-Reset" "731", "X-Rate-Limit-Remaining" "2", "X-Rate-Limit-Limit" "5"}}
{:status 200, :body "Hello world", :headers {"X-Rate-Limit-Reset" "729", "X-Rate-Limit-Remaining" "1", "X-Rate-Limit-Limit" "5"}}
{:status 200, :body "Hello world", :headers {"X-Rate-Limit-Reset" "728", "X-Rate-Limit-Remaining" "0", "X-Rate-Limit-Limit" "5"}}
{:status 429, :body "Too many requests", :headers {"X-Rate-Limit-Reset" "726", "X-Rate-Limit-Remaining" "0", "X-Rate-Limit-Limit" "5"}}
{:status 429, :body "Too many requests", :headers {"X-Rate-Limit-Reset" "705", "X-Rate-Limit-Remaining" "0", "X-Rate-Limit-Limit" "5"}}
{:status 200, :body "Hello world", :headers {"X-Rate-Limit-Reset" "887", "X-Rate-Limit-Remaining" "4", "X-Rate-Limit-Limit" "5"}}

When a request comes in, Redis is first PINGed to ensure availability. Then it sets (HINCRBY "ratelimits" "172.16.254.1" +1), assocs X-Rate-Limit-* headers, and either shortcircuits with a 429 or passes the request down the rest of the middleware stack.

If the Redis value at "ratelimits:window" is not equal to the calculated window, then it deletes the "ratelimits" hashmap and sets "ratelimits:window" to the current window.

I like the idea of windowing on subintervals per-hour, per-minute, and per-second, though it imposes some complexity on the client. I suppose you'd return X-Rate-Limit-* headers for the longest interval (per-hour) and use a more granular error when subinterval-limits are expired.