- Updated: March 18, 2026
- 5 min read
Implementing a Distributed Token‑Bucket Rate Limiter for the OpenClaw Rating API with a Multi‑Node Redis Cluster
Implementing a Distributed Token‑Bucket Rate Limiter for the OpenClaw Rating API
Rate limiting is essential for protecting the OpenClaw Rating API from abuse, ensuring fair usage, and keeping latency low. In this guide we walk developers through a complete, step‑by‑step implementation of a distributed token‑bucket limiter using a multi‑node Redis Cluster. The solution works at the edge, scales horizontally, tolerates node failures, and follows best‑practice patterns.
Table of Contents
- Architecture Overview
- Prerequisites
- Setting Up a Redis Cluster
- Edge Deployment with NGINX + Lua (or Envoy)
- Token‑Bucket Algorithm in Redis
- Scaling Considerations
- Fault‑Tolerance & High Availability
- Best‑Practice Patterns
- Testing the Limiter
- Publishing the Blog Post
1. Architecture Overview
The limiter sits at the edge (NGINX, Envoy, or Cloudflare Workers) and forwards a GET request to a Redis Cluster to acquire a token. If a token is granted, the request proceeds to the OpenClaw Rating API; otherwise a 429 Too Many Requests response is returned.
2. Prerequisites
- Ubuntu 22.04+ (or any Linux distro)
- Docker & Docker‑Compose
- Redis 7.x
- NGINX 1.21+ with the
lua‑redismodule (or Envoy with Lua filter) - Access to the UBOS platform (for publishing the blog)
3. Setting Up a Redis Cluster
# docker‑compose.yml
version: "3"
services:
redis-node-1:
image: redis:7-alpine
command: ["redis-server", "--cluster-enabled", "yes", "--cluster-config-file", "nodes.conf", "--appendonly", "yes"]
ports: ["6379:6379"]
redis-node-2:
image: redis:7-alpine
command: ["redis-server", "--cluster-enabled", "yes", "--cluster-config-file", "nodes.conf", "--appendonly", "yes"]
ports: ["6380:6379"]
redis-node-3:
image: redis:7-alpine
command: ["redis-server", "--cluster-enabled", "yes", "--cluster-config-file", "nodes.conf", "--appendonly", "yes"]
ports: ["6381:6379"]
After starting the containers run:
docker exec -it redis-node-1 redis-cli --cluster create 172.17.0.2:6379 172.17.0.3:6379 172.17.0.4:6379 --cluster-replicas 0
This creates a 3‑node master‑only cluster. For production add replicas (e.g., --cluster-replicas 1).
4. Edge Deployment with NGINX + Lua
Install the lua‑resty‑redis library and enable the ngx_http_lua_module.
# nginx.conf (excerpt)
http {
lua_shared_dict limits 10m;
server {
listen 80;
location /rating {
access_by_lua_block {
local redis = require "resty.redis"
local red = redis:new()
red:set_timeout(100) -- 100 ms
local ok, err = red:connect("redis-node-1", 6379)
if not ok then
ngx.log(ngx.ERR, "Redis connect failed: ", err)
return ngx.exit(ngx.HTTP_SERVICE_UNAVAILABLE)
end
-- Token‑bucket parameters (per‑client key)
local key = "rl:" .. ngx.var.remote_addr
local limit = 100 -- max requests per window
local interval = 60 -- window in seconds
local token, ttl = red:eval([[
local key = KEYS[1]
local limit = tonumber(ARGV[1])
local interval = tonumber(ARGV[2])
local now = redis.call('TIME')[1]
local bucket = redis.call('HMGET', key, 'tokens', 'ts')
local tokens = tonumber(bucket[1])
local ts = tonumber(bucket[2])
if not tokens then
tokens = limit
ts = now
end
local elapsed = now - ts
local refill = (elapsed / interval) * limit
tokens = math.min(limit, tokens + refill)
if tokens < 1 then
redis.call('HMSET', key, 'tokens', tokens, 'ts', now)
redis.call('EXPIRE', key, interval)
return {0, redis.call('TTL', key)}
else
tokens = tokens - 1
redis.call('HMSET', key, 'tokens', tokens, 'ts', now)
redis.call('EXPIRE', key, interval)
return {1, redis.call('TTL', key)}
end
]], 1, key, limit, interval)
if token == 1 then
-- token granted, continue to upstream
return
else
ngx.header["Retry-After"] = ttl
return ngx.exit(429)
end
}
proxy_pass http://openclaw-rating-api;
}
}
}
The Lua script implements the classic token‑bucket algorithm in a single atomic EVAL call, guaranteeing consistency across the cluster.
5. Token‑Bucket Algorithm in Redis (Standalone Reference)
If you prefer a micro‑service instead of Lua, the same script can be exposed via a tiny Go/Node service that receives client_id and returns allowed boolean.
6. Scaling Considerations
- Sharding keys: Use a composite key like
rl:{region}:{client_id}to spread load across cluster slots. - Connection pooling: Re‑use Redis connections in NGINX Lua or in your service to avoid TCP‑handshake overhead.
- Horizontal edge nodes: Deploy NGINX/Envoy on multiple edge locations; each node talks to the same Redis Cluster, guaranteeing a global limit.
7. Fault‑Tolerance & High Availability
- Run at least 3 master nodes with replicas (minimum 6 nodes total) so a master can fail without losing data.
- Enable
cluster-require-full-coverage noto allow the cluster to stay available when a subset of slots is down. - Configure NGINX health‑checks for Redis endpoints; on failure, fallback to another node.
8. Best‑Practice Patterns
- Idempotent limits: Use the client’s IP or API key as the bucket identifier.
- Graceful degradation: When Redis is unavailable, return
503 Service Unavailableinstead of silently allowing unlimited traffic. - Observability: Export Redis keyspace notifications or Lua metrics to Prometheus for real‑time monitoring.
- Security: Protect Redis with TLS and ACLs; only allow the edge nodes to connect.
9. Testing the Limiter
Use hey or ab to fire bursts of requests and verify that the 429 response appears after the configured limit.
hey -n 200 -c 20 http://your‑edge‑host/rating
10. Publishing the Blog Post
Now that the article is ready, we push it to the UBOS WordPress instance using the /blog endpoint. The post will appear under the “POST” type, with the featured image shown on the blog’s front page.
Happy coding! 🚀