Rate Limiting in Lua

Here’s the translation of the Go rate limiting example to Lua, formatted in Markdown suitable for Hugo:

Rate limiting is an important mechanism for controlling resource utilization and maintaining quality of service. Lua can support rate limiting using coroutines and timers.

local clock = os.clock

-- Simulate a simple channel
local function make_channel(size)
    local channel = {}
    local queue = {}
    local closed = false

    function channel.send(value)
        if closed then error("send on closed channel") end
        if #queue < size then
            table.insert(queue, value)
        else
            coroutine.yield()
        end
    end

    function channel.receive()
        if #queue == 0 and closed then return nil end
        while #queue == 0 do
            coroutine.yield()
        end
        return table.remove(queue, 1)
    end

    function channel.close()
        closed = true
    end

    return channel
end

-- Simulate time.Tick
local function tick(interval)
    local channel = make_channel(1)
    local function ticker()
        while true do
            channel.send(clock())
            coroutine.yield(interval)
        end
    end
    coroutine.wrap(ticker)()
    return channel
end

local function 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.
    local requests = make_channel(5)
    for i = 1, 5 do
        requests.send(i)
    end
    requests.close()

    -- This limiter channel will receive a value
    -- every 200 milliseconds. This is the regulator in
    -- our rate limiting scheme.
    local limiter = tick(0.2)

    -- By blocking on a receive from the limiter channel
    -- before serving each request, we limit ourselves to
    -- 1 request every 200 milliseconds.
    while true do
        local req = requests.receive()
        if not req then break end
        limiter.receive()
        print(string.format("request %d %f", req, clock()))
    end

    -- 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.
    local burstyLimiter = make_channel(3)

    -- Fill up the channel to represent allowed bursting.
    for i = 1, 3 do
        burstyLimiter.send(clock())
    end

    -- Every 200 milliseconds we'll try to add a new
    -- value to burstyLimiter, up to its limit of 3.
    coroutine.wrap(function()
        for t in coroutine.wrap(function() while true do coroutine.yield(tick(0.2).receive()) end end) do
            burstyLimiter.send(t)
        end
    end)()

    -- Now simulate 5 more incoming requests. The first
    -- 3 of these will benefit from the burst capability
    -- of burstyLimiter.
    local burstyRequests = make_channel(5)
    for i = 1, 5 do
        burstyRequests.send(i)
    end
    burstyRequests.close()

    while true do
        local req = burstyRequests.receive()
        if not req then break end
        burstyLimiter.receive()
        print(string.format("request %d %f", req, clock()))
    end
end

main()

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

$ lua rate-limiting.lua
request 1 0.000039
request 2 0.200161
request 3 0.400284
request 4 0.600407
request 5 0.800530

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

request 1 0.800568
request 2 0.800576
request 3 0.800582
request 4 1.000705
request 5 1.200828

This Lua implementation simulates the behavior of Go’s channels and goroutines using coroutines and custom channel implementations. The tick function mimics Go’s time.Tick, and the make_channel function creates a simple channel-like structure. The overall logic of rate limiting remains the same, demonstrating both basic and bursty rate limiting.