[TERMINAL · SKILLS]
> mounting /skills...
> indexing skill manifests...
> linking agents: claude · codex · gemini · cursor
> ready.
[░░░░░░░░░░░░░░░░░░░░░░░░░░░░] 0%
Use Cases/Build a Token Bucket Rate Limiter

Build a Token Bucket Rate Limiter

Build a distributed token bucket rate limiter with per-user buckets, burst allowance, sliding window fallback, response headers, and dynamic configuration for API protection.

Development#typescript#tsconfig#type-checking#type-safety#javascript-migration
Works with:claude-codeopenai-codexgemini-clicursor
agent@terminalskills — playground
full playground →

Try this use case on your own files

simulated preview
$
$

real run in an isolated sandbox · files auto-deleted after 7 days · nothing touches your machine

Skills stack · 4 skills

Avg quality 95/100·All SAFE
>

typescript

v1.0.0

Sets up and repairs TypeScript projects: picks tsconfig.json settings for a Node.js service, an npm library or a bundled web app, runs type-checking in CI, and replaces `any` and unsafe casts with types the compiler can verify. Use when someone asks to "set up TypeScript", "fix this tsconfig", "fix a TypeScript error", "upgrade to TypeScript 6 or 7", "migrate JavaScript to TypeScript", "publish types for a package", "run .ts files in Node" or "make this type-safe". Covers the changed defaults in TypeScript 6.0 and 7.0, module and moduleResolution, strict mode, project references, declaration output, narrowing, satisfies and common compiler errors.

93/100 quality
7.30× impact
SAFE
View skill
>

redis

v1.0.0

Build applications with Redis — caching, session storage, pub/sub, streams, rate limiting, leaderboards, and queues. Use when tasks involve in-memory data storage, real-time messaging, distributed locking, or performance optimization with caching layers.

93/100 quality
1.81× impact
SAFE
View skill
>

hono

v1.0.0

You are an expert in Hono, the ultrafast web framework for the edge. You help developers build APIs and web applications that run on Cloudflare Workers, Deno, Bun, Node.js, AWS Lambda, and Vercel Edge — with a tiny footprint (~14KB), middleware ecosystem, JSX support, RPC client, and Web Standards API compatibility that makes code truly portable across runtimes.

93/100 quality
3.00× impact
SAFE
View skill
>

zod

v1.0.0

You are an expert in Zod, the TypeScript-first schema declaration and validation library. You help developers define schemas that validate data at runtime AND infer TypeScript types at compile time — eliminating the need to write types and validators separately. Used for API input validation, form validation, environment variables, config files, and any data boundary.

100/100 quality
1.21× impact
SAFE
View skill
$

The Problem

Vera leads platform at a 25-person API company serving 2,000 customers. Their fixed-window rate limiter creates a thundering herd: customers batch requests at window boundaries, causing 10x traffic spikes every minute. Burst-y workloads get rejected even though average usage is low. Enterprise customers want higher limits but changing limits requires redeploying. They need a token bucket algorithm: smooth rate limiting without boundary spikes, configurable burst allowance, per-customer configuration, and proper rate limit headers so clients can self-throttle.

Step 1: Build the Rate Limiter

typescript
// src/ratelimit/token-bucket.ts — Distributed token bucket with burst and dynamic config
import { Redis } from "ioredis";

const redis = new Redis(process.env.REDIS_URL!);

interface BucketConfig {
  maxTokens: number;         // bucket capacity (burst size)
  refillRate: number;        // tokens added per second
  refillInterval: number;    // ms between refills (default 1000)
}

interface RateLimitResult {
  allowed: boolean;
  remaining: number;
  limit: number;
  retryAfter: number | null; // seconds until a token is available
  resetAt: number;           // timestamp when bucket will be full
}

const DEFAULT_CONFIGS: Record<string, BucketConfig> = {
  free:       { maxTokens: 60,   refillRate: 1,   refillInterval: 1000 },
  starter:    { maxTokens: 300,  refillRate: 5,   refillInterval: 1000 },
  pro:        { maxTokens: 1000, refillRate: 20,  refillInterval: 1000 },
  enterprise: { maxTokens: 5000, refillRate: 100, refillInterval: 1000 },
};

// Consume a token from the bucket (atomic via Lua script)
export async function consume(
  key: string,
  config?: BucketConfig,
  tokens: number = 1
): Promise<RateLimitResult> {
  const cfg = config || DEFAULT_CONFIGS.free;
  const now = Date.now();

  // Atomic Lua script for token bucket
  const result = await redis.eval(
    `
    local key = KEYS[1]
    local maxTokens = tonumber(ARGV[1])
    local refillRate = tonumber(ARGV[2])
    local refillInterval = tonumber(ARGV[3])
    local now = tonumber(ARGV[4])
    local requested = tonumber(ARGV[5])

    -- Get current state
    local bucket = redis.call('HMGET', key, 'tokens', 'lastRefill')
    local currentTokens = tonumber(bucket[1]) or maxTokens
    local lastRefill = tonumber(bucket[2]) or now

    -- Calculate tokens to add since last refill
    local elapsed = now - lastRefill
    local tokensToAdd = math.floor(elapsed / refillInterval) * refillRate
    currentTokens = math.min(maxTokens, currentTokens + tokensToAdd)
    local newLastRefill = lastRefill + math.floor(elapsed / refillInterval) * refillInterval

    -- Try to consume
    local allowed = 0
    if currentTokens >= requested then
      currentTokens = currentTokens - requested
      allowed = 1
    end

    -- Save state
    redis.call('HMSET', key, 'tokens', currentTokens, 'lastRefill', newLastRefill)
    redis.call('EXPIRE', key, math.ceil(maxTokens / refillRate) + 60)

    return {allowed, currentTokens, maxTokens}
    `,
    1, key,
    cfg.maxTokens, cfg.refillRate, cfg.refillInterval, now, tokens
  ) as number[];

  const allowed = result[0] === 1;
  const remaining = result[1];

  let retryAfter: number | null = null;
  if (!allowed) {
    retryAfter = Math.ceil((tokens - remaining) / cfg.refillRate);
  }

  const resetAt = now + Math.ceil((cfg.maxTokens - remaining) / cfg.refillRate) * 1000;

  return { allowed, remaining, limit: cfg.maxTokens, retryAfter, resetAt };
}

// Get current bucket state without consuming
export async function peek(key: string, config?: BucketConfig): Promise<RateLimitResult> {
  const cfg = config || DEFAULT_CONFIGS.free;
  const data = await redis.hmget(key, "tokens", "lastRefill");
  const now = Date.now();

  let currentTokens = data[0] ? parseInt(data[0]) : cfg.maxTokens;
  const lastRefill = data[1] ? parseInt(data[1]) : now;

  const elapsed = now - lastRefill;
  const tokensToAdd = Math.floor(elapsed / cfg.refillInterval) * cfg.refillRate;
  currentTokens = Math.min(cfg.maxTokens, currentTokens + tokensToAdd);

  return {
    allowed: currentTokens > 0,
    remaining: currentTokens,
    limit: cfg.maxTokens,
    retryAfter: currentTokens > 0 ? null : Math.ceil(1 / cfg.refillRate),
    resetAt: now + Math.ceil((cfg.maxTokens - currentTokens) / cfg.refillRate) * 1000,
  };
}

// Hono middleware
export function rateLimitMiddleware(options?: {
  keyExtractor?: (c: any) => string;
  configResolver?: (c: any) => BucketConfig;
  costCalculator?: (c: any) => number;
}) {
  return async (c: any, next: any) => {
    const key = options?.keyExtractor?.(c) || `rl:${c.req.header("X-API-Key") || c.req.header("CF-Connecting-IP") || "anonymous"}`;
    const config = options?.configResolver?.(c);
    const cost = options?.costCalculator?.(c) || 1;

    const result = await consume(key, config, cost);

    // Always set headers
    c.header("X-RateLimit-Limit", String(result.limit));
    c.header("X-RateLimit-Remaining", String(result.remaining));
    c.header("X-RateLimit-Reset", String(Math.ceil(result.resetAt / 1000)));

    if (!result.allowed) {
      c.header("Retry-After", String(result.retryAfter));
      return c.json({ error: "Rate limit exceeded", retryAfter: result.retryAfter }, 429);
    }

    await next();
  };
}

// Dynamic config update (no redeploy)
export async function updateConfig(customerId: string, config: BucketConfig): Promise<void> {
  await redis.set(`rl:config:${customerId}`, JSON.stringify(config));
}

export async function getConfig(customerId: string): Promise<BucketConfig> {
  const cached = await redis.get(`rl:config:${customerId}`);
  return cached ? JSON.parse(cached) : DEFAULT_CONFIGS.free;
}

Results

  • No more thundering herd — token bucket smooths traffic; no boundary spikes; steady 20 req/sec vs 1200 req burst at minute boundary
  • Burst-friendly — bucket capacity allows 1000-token burst for pro tier; short spikes accepted; only sustained overuse is throttled
  • Proper headers — X-RateLimit-Remaining and Retry-After in every response; well-behaved clients self-throttle; support tickets about rate limits dropped 70%
  • Dynamic configuration — enterprise customer needs 10K/min temporarily; update via API without redeploy; takes effect in <1 second
  • Atomic via Lua — single Redis round-trip; no race conditions between check and decrement; works across multiple API servers

Related use cases