Rate Limiting in Idris

Rate limiting is an important mechanism for controlling resource utilization and maintaining quality of service. Idris supports rate limiting through its powerful type system and effect handling capabilities.

import Data.IORef
import System.Concurrency
import System.Clock

-- 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 list of the same name.
main : IO ()
main = do
  requests <- newIORef [1, 2, 3, 4, 5]

  -- This limiter function will introduce a delay
  -- every 200 milliseconds. This is the regulator in
  -- our rate limiting scheme.
  let limiter = do
        threadDelay 200000  -- 200 milliseconds
        pure ()

  -- By calling the limiter function before serving each request,
  -- we limit ourselves to 1 request every 200 milliseconds.
  let processRequests = do
        reqs <- readIORef requests
        case reqs of
          [] -> pure ()
          (req::rs) => do
            limiter
            putStrLn $ "request " ++ show req ++ " " ++ show !time
            writeIORef requests rs
            processRequests

  -- Process the requests
  processRequests

  -- 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
  -- using a semaphore. This burstyLimiter will allow
  -- bursts of up to 3 events.
  burstyLimiter <- newSemaphore 3

  -- Fill up the semaphore to represent allowed bursting.
  replicateM_ 3 $ signal burstyLimiter

  -- Every 200 milliseconds we'll try to add a new
  -- value to burstyLimiter, up to its limit of 3.
  fork $ forever $ do
    threadDelay 200000
    signal burstyLimiter

  -- Now simulate 5 more incoming requests. The first
  -- 3 of these will benefit from the burst capability
  -- of burstyLimiter.
  burstyRequests <- newIORef [1, 2, 3, 4, 5]

  let processBurstyRequests = do
        reqs <- readIORef burstyRequests
        case reqs of
          [] -> pure ()
          (req::rs) => do
            wait burstyLimiter
            putStrLn $ "request " ++ show req ++ " " ++ show !time
            writeIORef burstyRequests rs
            processBurstyRequests

  -- Process the bursty requests
  processBurstyRequests

Running our program, we would see the first batch of requests handled once every ~200 milliseconds as desired.

For the second batch of requests, we would serve the first 3 immediately because of the burstable rate limiting, then serve the remaining 2 with ~200ms delays each.

Note that Idris doesn’t have built-in channels like Go, so we’ve used IORef for shared state and semaphores for synchronization. The fork function is used to start a new thread, similar to Go’s goroutines. The threadDelay function is used to introduce delays, measured in microseconds.

Also, Idris doesn’t have a built-in way to get the current time with microsecond precision, so we’ve used a placeholder time function. In a real implementation, you’d need to use a library or FFI to get the current time with high precision.

This example demonstrates how Idris’s powerful type system and effect handling can be used to implement rate limiting, even though the specific mechanisms differ from Go’s channels and tickers.