- Updated: March 18, 2026
- 5 min read
Distributed Token‑Bucket Rate Limiter for the OpenClaw Rating API using Redis Cluster
# Distributed Token‑Bucket Rate Limiter for the OpenClaw Rating API
*Author: UBOS Team*
—
## Introduction
Rate limiting is essential for protecting APIs from abuse and ensuring fair usage. The OpenClaw Rating API already has a single‑instance token‑bucket tutorial, but production deployments often require **high availability** and **low latency** across multiple edge regions. This guide shows you how to build a **distributed token‑bucket rate limiter** backed by a **Redis Cluster** that spans edge regions, and how to deploy it on UBOS.
—
## Prerequisites
– UBOS 2.x installed on your edge nodes.
– A Redis Cluster (minimum 3 master nodes) reachable from each UBOS node.
– Access to the OpenClaw Rating API source code.
– Basic knowledge of Docker and Kubernetes (optional but helpful).
—
## 1. Review of the Single‑Instance Tutorial
The original tutorial creates a local in‑memory bucket:
go
type TokenBucket struct {
capacity int64
tokens int64
rate int64 // tokens per second
lastRefill time.Time
}
While simple, this approach does **not** survive node failures or scale across regions. We will replace the in‑memory store with a **Redis‑backed bucket**.
—
## 2. Cross‑Region Consistency Guide Recap
The cross‑region guide recommends:
– Using a **Redis Cluster** with geo‑distributed replicas.
– Leveraging **client‑side sharding** to route keys to the correct master.
– Configuring **replication latency alerts**.
Our implementation follows these recommendations.
—
## 3. Architecture Overview
+——————-+ +——————-+ +——————-+
| Edge Node A | | Edge Node B | | Edge Node C |
| (UBOS + App) | | (UBOS + App) | | (UBOS + App) |
+——–+———-+ +——–+———-+ +——–+———-+
| | |
| Redis Cluster (3 masters, 3 replicas) |
+—————————————————+
Each request to the OpenClaw Rating API first contacts the **local UBOS instance**, which then performs an atomic Lua script against the Redis Cluster to check and consume tokens.
—
## 4. Implementing the Distributed Token Bucket
### 4.1 Lua Script for Atomicity
Redis Lua scripts run atomically, guaranteeing that token checks are race‑free.
lua
— KEYS[1] = bucket key (e.g., “rate_limit:{client_id}”)
— ARGV[1] = capacity
— ARGV[2] = refill_rate (tokens per second)
— ARGV[3] = now (epoch seconds)
local capacity = tonumber(ARGV[1])
local refill_rate = tonumber(ARGV[2])
local now = tonumber(ARGV[3])
local bucket = redis.call(‘HMGET’, KEYS[1], ‘tokens’, ‘timestamp’)
local tokens = tonumber(bucket[1])
local timestamp = tonumber(bucket[2])
if tokens == nil then
tokens = capacity
timestamp = now
else
local elapsed = now – timestamp
local refill = elapsed * refill_rate
tokens = math.min(capacity, tokens + refill)
timestamp = now
end
if tokens < 1 then
— Not enough tokens
redis.call('HMSET', KEYS[1], 'tokens', tokens, 'timestamp', timestamp)
return 0
else
— Consume a token
tokens = tokens – 1
redis.call('HMSET', KEYS[1], 'tokens', tokens, 'timestamp', timestamp)
return 1
end
### 4.2 Go Wrapper
go
package ratelimit
import (
"github.com/go-redis/redis/v8"
"context"
"time"
)
type RedisBucket struct {
client *redis.ClusterClient
key string
capacity int64
rate int64 // tokens per second
}
func NewRedisBucket(client *redis.ClusterClient, key string, capacity, rate int64) *RedisBucket {
return &RedisBucket{client: client, key: key, capacity: capacity, rate: rate}
}
func (b *RedisBucket) Allow(ctx context.Context) (bool, error) {
script := redis.NewScript(luaScript) // luaScript is the script above as a string
now := time.Now().Unix()
res, err := script.Run(ctx, b.client, []string{b.key}, b.capacity, b.rate, now).Result()
if err != nil {
return false, err
}
allowed, ok := res.(int64)
return ok && allowed == 1, nil
}
### 4.3 Integrating with the OpenClaw Service
go
func RateLimitedHandler(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
clientID := r.Header.Get("X-Client-ID")
bucketKey := fmt.Sprintf("rate_limit:%s", clientID)
bucket := NewRedisBucket(redisCluster, bucketKey, 100, 10) // 100 requests burst, 10 rps
allowed, err := bucket.Allow(ctx)
if err != nil || !allowed {
http.Error(w, "Too Many Requests", http.StatusTooManyRequests)
return
}
// Forward to actual rating logic
handleRating(w, r)
}
—
## 5. Deploying on UBOS
### 5.1 Create a Docker Image
Dockerfile
FROM golang:1.22-alpine AS builder
WORKDIR /app
COPY . .
RUN go build -o rating-service ./cmd/rating
FROM alpine:latest
COPY –from=builder /app/rating-service /usr/local/bin/rating-service
EXPOSE 8080
ENTRYPOINT ["rating-service"]
Push the image to your registry.
### 5.2 UBOS Service Definition (`ubos.yaml`)
yaml
services:
rating-service:
image: your-registry/rating-service:latest
ports:
– "8080:8080"
env:
REDIS_CLUSTER: "redis-cluster:6379"
depends_on:
– redis-cluster
redis-cluster:
image: redis:7.2-alpine
command: ["redis-server", "–cluster-enabled", "yes", "–cluster-config-file", "nodes.conf", "–appendonly", "yes"]
ports:
– "6379:6379"
volumes:
– redis-data:/data
volumes:
redis-data:
Deploy with:
ubos apply -f ubos.yaml
UBOS will automatically schedule the services across your edge nodes, ensuring the Redis Cluster is spread geographically.
—
## 6. Testing the Rate Limiter
bash
# Simulate 120 requests in quick succession
for i in {1..120}; do curl -s -o /dev/null -w "%{http_code}\n" -H "X-Client-ID: test-client" http:///rate; done
You should see a mix of `200` (allowed) and `429` (Too Many Requests) once the bucket is exhausted.
—
## 7. Monitoring & Alerts
– **Redis INFO** for latency metrics.
– UBOS built‑in Prometheus exporter can scrape `redis_cluster_*` metrics.
– Set up alerts when `rate_limit:rejection_rate` exceeds a threshold.
—
## 8. Conclusion
By moving the token bucket into a Redis Cluster, you gain:
– **High availability** – no single point of failure.
– **Geographic distribution** – low latency for edge users.
– **Consistent enforcement** across all UBOS nodes.
Feel free to adapt the bucket parameters to match your API’s traffic profile.
—
[Read more about hosting OpenClaw on UBOS](/host-openclaw/)
—
*Published on UBOS Tech*