skills-ws
Version:
87 agent skills for AI coding assistants — marketing, growth, web3, dev, design & operations. Built for OpenClaw, Claude Code, Cursor, and Codex.
174 lines (142 loc) • 6.06 kB
Markdown
## Contents
- Rate Limiting
- Sliding Window Log with Redis (Production)
- Atomic Token Bucket (Lua) — constant memory, allows bursts
## Rate Limiting
### Sliding Window Log with Redis (Production)
A sorted set stores one member per request, scored by timestamp. Each call
trims entries older than the window, adds the current request, and counts
what remains — giving an exact rolling count with no fixed-window burst
seam. Cost is O(log N) per request and memory is O(requests-in-window) per
key, so for very high-volume limits prefer a token-bucket / GCRA counter
(constant memory) — see the atomic Lua variant below.
```typescript
import Redis from 'ioredis';
const redis = new Redis(process.env.REDIS_URL);
interface RateLimitResult {
allowed: boolean;
remaining: number;
resetAt: number;
retryAfter?: number;
}
async function checkRateLimit(
key: string,
maxRequests: number,
windowSeconds: number,
): Promise<RateLimitResult> {
const now = Math.floor(Date.now() / 1000);
const windowStart = now - windowSeconds;
// Sliding window log using sorted set
const pipeline = redis.pipeline();
pipeline.zremrangebyscore(key, 0, windowStart); // Remove old entries
pipeline.zadd(key, now.toString(), `${now}:${Math.random()}`); // Add current
pipeline.zcard(key); // Count in window
pipeline.expire(key, windowSeconds); // TTL cleanup
const results = await pipeline.exec();
const count = results![2][1] as number;
if (count > maxRequests) {
const oldestInWindow = await redis.zrange(key, 0, 0, 'WITHSCORES');
const retryAfter = oldestInWindow.length >= 2
? parseInt(oldestInWindow[1]) + windowSeconds - now
: windowSeconds;
return {
allowed: false,
remaining: 0,
resetAt: now + retryAfter,
retryAfter,
};
}
return {
allowed: true,
remaining: maxRequests - count,
resetAt: now + windowSeconds,
};
}
// Middleware
function rateLimit(maxRequests: number, windowSeconds: number) {
return async (req: Request, res: Response, next: NextFunction) => {
// Per-user if authenticated, per-IP otherwise
const key = req.user
? `ratelimit:user:${req.user.id}`
: `ratelimit:ip:${req.ip}`;
const result = await checkRateLimit(key, maxRequests, windowSeconds);
// IETF draft headers (draft-ietf-httpapi-ratelimit-headers, still an
// Internet-Draft, not an RFC). The latest draft consolidates these into
// RateLimit and RateLimit-Policy structured fields; the Limit/Remaining/Reset
// trio below matches earlier drafts and stays the most widely deployed form.
// `RateLimit-Reset` is seconds-until-reset
// (a delta), not an epoch timestamp — that's the key difference from the
// legacy `X-RateLimit-Reset` convention below.
const resetDelta = Math.max(0, result.resetAt - Math.floor(Date.now() / 1000));
res.setHeader('RateLimit-Limit', maxRequests);
res.setHeader('RateLimit-Remaining', result.remaining);
res.setHeader('RateLimit-Reset', resetDelta);
// Legacy headers — keep for older clients; `X-RateLimit-Reset` is an epoch.
res.setHeader('X-RateLimit-Limit', maxRequests);
res.setHeader('X-RateLimit-Remaining', result.remaining);
res.setHeader('X-RateLimit-Reset', result.resetAt);
if (!result.allowed) {
res.setHeader('Retry-After', result.retryAfter!); // seconds (RFC 9110)
throw new RateLimitError(result.retryAfter!);
}
next();
};
}
// Different limits for different endpoints
app.use('/api/v1/auth', rateLimit(10, 60)); // 10/min for auth
app.use('/api/v1/', rateLimit(100, 60)); // 100/min general
app.use('/api/v1/search', rateLimit(30, 60)); // 30/min for search
```
### Atomic Token Bucket (Lua) — constant memory, allows bursts
The sliding-window pipeline above is two round-trips and stores one key per
request. A token bucket runs as a single atomic Lua script (no race between
read and write under concurrency), uses O(1) memory per key, and naturally
permits short bursts up to `capacity` while enforcing a steady refill rate.
```typescript
// Refills `refillRate` tokens/sec up to `capacity`; each request costs 1 token.
// KEYS[1] = bucket key. ARGV: capacity, refillRate, now (sec, fractional), cost.
const TOKEN_BUCKET = `
local key = KEYS[1]
local capacity = tonumber(ARGV[1])
local refillRate = tonumber(ARGV[2])
local now = tonumber(ARGV[3])
local cost = tonumber(ARGV[4])
local state = redis.call('HMGET', key, 'tokens', 'ts')
local tokens = tonumber(state[1])
local ts = tonumber(state[2])
if tokens == nil then tokens = capacity; ts = now end
-- Refill based on elapsed time, cap at capacity
tokens = math.min(capacity, tokens + (now - ts) * refillRate)
local allowed = 0
if tokens >= cost then
allowed = 1
tokens = tokens - cost
end
redis.call('HSET', key, 'tokens', tokens, 'ts', now)
-- Expire when the bucket would be full again (idle reclaim)
redis.call('EXPIRE', key, math.ceil(capacity / refillRate) + 1)
-- Seconds until enough tokens for one request (0 if allowed now)
local retry = 0
if allowed == 0 then retry = (cost - tokens) / refillRate end
return { allowed, tostring(tokens), tostring(retry) }
`;
const sha = await redis.script('LOAD', TOKEN_BUCKET);
async function checkTokenBucket(
key: string, capacity: number, refillRate: number, cost = 1,
): Promise<RateLimitResult> {
const now = Date.now() / 1000;
const [allowed, tokensStr, retryStr] = (await redis.evalsha(
sha, 1, key, capacity, refillRate, now, cost,
)) as [number, string, string];
const remaining = Math.floor(parseFloat(tokensStr));
const retryAfter = Math.ceil(parseFloat(retryStr));
return {
allowed: allowed === 1,
remaining,
resetAt: Math.floor(now) + Math.ceil((capacity - remaining) / refillRate),
...(allowed === 1 ? {} : { retryAfter }),
};
}
// e.g. checkTokenBucket('ratelimit:user:42', 100, 100 / 60) → 100 burst, refills to 100/min
```
---