sempadan, an open source rate limiter
A small rate limiter that gives the same answer from Go and Node, backed by one Redis script.
4.3k
- Open source
- Author and maintainer
- since 2022
- 2022.11, v2.6
- Go, TypeScript, Redis, Lua
Problem
Most teams I worked with had a Go service and a Node service guarding the same API, each with its own rate limiter. They disagreed at the edges, so a client could get through on one and be blocked on the other within the same second.
sempadan, Malay for boundary, is one token bucket written once in Lua and run inside Redis, with thin clients in Go and TypeScript that call it the same way.
Constraints
- One round trip per check. A limiter that adds 5 ms to every request will be removed.
- Correct under clock skew between app servers, so time comes from Redis, not the caller.
- No dependencies beyond the Redis client each language already has.
Architecture
Code
The whole algorithm. Redis TIME is the only clock, so two app servers can never disagree.
local key, rate, burst, cost = KEYS[1], tonumber(ARGV[1]), tonumber(ARGV[2]), tonumber(ARGV[3])local t = redis.call("TIME")local now = t[1] * 1000 + math.floor(t[2] / 1000)local b = redis.call("HMGET", key, "tokens", "ts")local tokens = tonumber(b[1]) or burstlocal last = tonumber(b[2]) or nowtokens = math.min(burst, tokens + (now - last) * rate / 1000)local ok = tokens >= costif ok then tokens = tokens - cost endredis.call("HSET", key, "tokens", tokens, "ts", now)redis.call("PEXPIRE", key, math.ceil(burst / rate * 1000) + 1000)return { ok and 1 or 0, math.floor(tokens) }
Result
- median added latency per check
- 0.21 ms
- GitHub stars
- 4.3k
- contributors
- 61
- monthly npm downloads
- 410k
Version 3 adds sliding windows and a test harness that runs the same cases against both clients on every pull request.