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:

  1. We use clojure.core.async for channel operations and concurrency.

  2. The requests channel simulates incoming requests.

  3. The limiter is created using a/ticker, which sends a value every 200 milliseconds.

  4. We use doseq and a/<!! to iterate over the requests, waiting for the limiter before processing each one.

  5. For bursty rate limiting, we create a bursty-limiter channel with a buffer of 3.

  6. We use a/go-loop to continuously try to add new values to the bursty-limiter every 200 milliseconds.

  7. 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.