Rate Limiting in Clojure
(ns rate-limiting
(:require [clojure.core.async :as a]
[clojure.java.io :as io]))
(defn now []
(java.time.LocalDateTime/now))
(defn main []
; First we'll look at basic rate limiting. Suppose
; we want to limit our handling of incoming requests.
; We'll serve these requests off a channel of the
; same name.
(let [requests (a/chan 5)]
(doseq [i (range 1 6)]
(a/>!! requests i))
(a/close! requests)
; This limiter channel will receive a value
; every 200 milliseconds. This is the regulator in
; our rate limiting scheme.
(let [limiter (a/ticker 200)]
; By blocking on a receive from the limiter channel
; before serving each request, we limit ourselves to
; 1 request every 200 milliseconds.
(doseq [req (a/<!! (a/into [] requests))]
(a/<!! limiter)
(println "request" req (now))))
; We may want to allow short bursts of requests in
; our rate limiting scheme while preserving the
; overall rate limit. We can accomplish this by
; buffering our limiter channel. This burstyLimiter
; channel will allow bursts of up to 3 events.
(let [bursty-limiter (a/chan 3)]
; Fill up the channel to represent allowed bursting.
(dotimes [_ 3]
(a/>!! bursty-limiter (now)))
; Every 200 milliseconds we'll try to add a new
; value to burstyLimiter, up to its limit of 3.
(a/go-loop []
(when (a/>! bursty-limiter (now))
(a/<! (a/timeout 200))
(recur)))
; Now simulate 5 more incoming requests. The first
; 3 of these will benefit from the burst capability
; of burstyLimiter.
(let [bursty-requests (a/chan 5)]
(doseq [i (range 1 6)]
(a/>!! bursty-requests i))
(a/close! bursty-requests)
(doseq [req (a/<!! (a/into [] bursty-requests))]
(a/<!! bursty-limiter)
(println "request" req (now))))))
(main)
This Clojure code implements rate limiting using core.async channels, which provide similar functionality to Go’s channels and goroutines. Here’s an explanation of the key parts:
We use
clojure.core.async
for channel operations and concurrency.The
requests
channel simulates incoming requests.The
limiter
is created usinga/ticker
, which sends a value every 200 milliseconds.We use
doseq
anda/<!!
to iterate over the requests, waiting for the limiter before processing each one.For bursty rate limiting, we create a
bursty-limiter
channel with a buffer of 3.We use
a/go-loop
to continuously try to add new values to thebursty-limiter
every 200 milliseconds.Finally, we simulate bursty requests and process them using the
bursty-limiter
.
Running this program will show the first batch of requests handled once every ~200 milliseconds as desired. For the second batch of requests, the first 3 will be served immediately due to the burstable rate limiting, then the remaining 2 with ~200ms delays each.
Note that Clojure’s concurrency model is different from Go’s, but core.async provides similar abstractions that allow us to implement this rate limiting pattern effectively.