Skip to content

Demo: This is a demo template by Ruji Labs. The business, people, products, prices and reviews on this page are fictional and do not represent any real company.

zahin ooi
menu

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

stars, 61 contributors

Client
Open source
Role
Author and maintainer
Duration
since 2022
Shipped
2022.11, v2.6
Stack
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

3 that shaped the design

  • 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

scroll sideways to see all of it

The bucket logic lives in Redis. Clients in either language only pass a key, a rate and a cost.
key, rate, costGo servicesempadan-goNode service@sempadan/nodebucket.luaone EVALSHARedisTIME, HASHmetricsoptional

boxes are servicesdouble top rule is a storedashed is async or advisorygreen is what this project built

Code

excerpt, lightly trimmed

The whole algorithm. Redis TIME is the only clock, so two app servers can never disagree.

scripts/bucket.lualua
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

measured, not estimated

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.

Demo only

This button is part of a demo template. Nothing was sent, booked or charged, and there is no real business behind this page.

Want a page like this for your business?

QR code: Darus Ishak, Ruji Labs contact cardScan or tap to get the contact card of Darus Ishak, Ruji Labs.Open contact card ↗