{"id":50216091,"url":"https://github.com/databuddy-analytics/better-ratelimit","last_synced_at":"2026-05-26T09:03:39.472Z","repository":{"id":305339692,"uuid":"1022624381","full_name":"databuddy-analytics/better-ratelimit","owner":"databuddy-analytics","description":null,"archived":false,"fork":false,"pushed_at":"2025-07-20T18:11:08.000Z","size":1472,"stargazers_count":17,"open_issues_count":1,"forks_count":1,"subscribers_count":1,"default_branch":"main","last_synced_at":"2026-03-05T12:58:35.807Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":"https://better-ratelimit-docs.vercel.app","language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/databuddy-analytics.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null}},"created_at":"2025-07-19T13:28:23.000Z","updated_at":"2026-03-03T07:31:05.000Z","dependencies_parsed_at":"2025-07-19T17:57:33.586Z","dependency_job_id":null,"html_url":"https://github.com/databuddy-analytics/better-ratelimit","commit_stats":null,"previous_names":["databuddy-analytics/better-ratelimit"],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/databuddy-analytics/better-ratelimit","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/databuddy-analytics%2Fbetter-ratelimit","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/databuddy-analytics%2Fbetter-ratelimit/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/databuddy-analytics%2Fbetter-ratelimit/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/databuddy-analytics%2Fbetter-ratelimit/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/databuddy-analytics","download_url":"https://codeload.github.com/databuddy-analytics/better-ratelimit/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/databuddy-analytics%2Fbetter-ratelimit/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":33512335,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T03:12:49.672Z","status":"ssl_error","status_checked_at":"2026-05-26T03:12:47.976Z","response_time":63,"last_error":"SSL_read: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2026-05-26T09:03:33.902Z","updated_at":"2026-05-26T09:03:39.466Z","avatar_url":"https://github.com/databuddy-analytics.png","language":"TypeScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"# better-ratelimit\n\n\u003e A **framework-agnostic**, **Effect-powered**, **observability-native** rate limiter designed for real-world infrastructure.\n\n[![Bun](https://img.shields.io/badge/Bun-000000?logo=bun\u0026logoColor=white)](https://bun.sh)\n[![TypeScript](https://img.shields.io/badge/TypeScript-3178C6?logo=typescript\u0026logoColor=white)](https://www.typescriptlang.org/)\n[![Effect](https://img.shields.io/badge/Effect-000000?logo=effect\u0026logoColor=white)](https://effect.website/)\n\n## 🚀 **Installation**\n\n```bash\nbun add better-ratelimit\n```\n\n## 📚 **Real-World Examples**\n\n### **1. API Rate Limiting**\n\n```typescript\nimport { createRedisRateLimiter } from \"better-ratelimit\"\n\n// API endpoint rate limiting: 100 requests per hour per user\nconst apiLimiter = createRedisRateLimiter(\n  process.env.REDIS_URL,\n  100,\n  \"1h\",\n  { \n    prefix: \"api:ratelimit\",\n    strategy: \"sliding-window\"\n  }\n)\n\n// In your API route handler\nasync function handleApiRequest(userId: string) {\n  const result = await apiLimiter.check(`user:${userId}`)\n  \n  if (!result.allowed) {\n    return {\n      error: \"Rate limit exceeded\",\n      retryAfter: new Date(result.resetTime).toISOString(),\n      remaining: result.remaining\n    }\n  }\n  \n  // Process the request...\n  return { success: true, remaining: result.remaining }\n}\n```\n\n### **2. Login Attempt Protection**\n\n```typescript\nimport { createRedisRateLimiter } from \"better-ratelimit\"\n\n// Login protection: 5 attempts per 15 minutes per IP\nconst loginLimiter = createRedisRateLimiter(\n  process.env.REDIS_URL,\n  5,\n  \"15m\",\n  { \n    prefix: \"login:ratelimit\",\n    strategy: \"fixed-window\"\n  }\n)\n\n// In your login endpoint\nasync function handleLogin(ip: string, email: string) {\n  const result = await loginLimiter.check(`ip:${ip}`)\n  \n  if (!result.allowed) {\n    return {\n      error: \"Too many login attempts\",\n      retryAfter: new Date(result.resetTime).toISOString()\n    }\n  }\n  \n  // Attempt login...\n  return { success: true }\n}\n```\n\n### **3. File Upload Limiting**\n\n```typescript\nimport { createMemoryRateLimiter } from \"better-ratelimit\"\n\n// File upload: 10 files per day per user\nconst uploadLimiter = createMemoryRateLimiter(10, \"24h\")\n\n// In your upload handler\nasync function handleFileUpload(userId: string) {\n  const result = await uploadLimiter.check(`upload:${userId}`)\n  \n  if (!result.allowed) {\n    return {\n      error: \"Upload limit reached\",\n      remaining: result.remaining,\n      resetDate: new Date(result.resetTime).toLocaleDateString()\n    }\n  }\n  \n  // Process upload...\n  return { success: true, remaining: result.remaining }\n}\n```\n\n### **4. Webhook Rate Limiting**\n\n```typescript\nimport { createRedisRateLimiter } from \"better-ratelimit\"\n\n// Webhook delivery: 1000 calls per hour per webhook\nconst webhookLimiter = createRedisRateLimiter(\n  process.env.REDIS_URL,\n  1000,\n  \"1h\",\n  { \n    prefix: \"webhook:ratelimit\",\n    strategy: \"approximated-sliding-window\"\n  }\n)\n\n// In your webhook sender\nasync function sendWebhook(webhookId: string, payload: any) {\n  const result = await webhookLimiter.check(`webhook:${webhookId}`)\n  \n  if (!result.allowed) {\n    console.log(`Webhook ${webhookId} rate limited, will retry later`)\n    return { queued: true }\n  }\n  \n  // Send webhook...\n  return { sent: true, remaining: result.remaining }\n}\n```\n\n### **5. Helper Methods \u0026 Common Patterns**\n\n```typescript\nimport { createRedisRateLimiter } from \"better-ratelimit\"\n\nconst limiter = createRedisRateLimiter(process.env.REDIS_URL, 100, \"1h\")\n\n// Simple boolean check\nif (await limiter.isAllowed(\"user:123\")) {\n  // Process request\n}\n\n// Get remaining requests\nconst remaining = await limiter.getRemaining(\"user:123\")\nconsole.log(`${remaining} requests remaining`)\n\n// Get reset time\nconst resetTime = await limiter.getResetTime(\"user:123\")\nconsole.log(`Resets at ${new Date(resetTime).toISOString()}`)\n\n// Get all info at once\nconst info = await limiter.getInfo(\"user:123\")\nif (!info.allowed) {\n  return {\n    error: \"Rate limit exceeded\",\n    remaining: info.remaining,\n    retryAfter: new Date(info.resetTime).toISOString()\n  }\n}\n```\n\n### **6. Middleware Pattern**\n\n```typescript\nimport { createRedisRateLimiter } from \"better-ratelimit\"\n\nconst apiLimiter = createRedisRateLimiter(process.env.REDIS_URL, 100, \"1h\")\n\n// Express-style middleware\nasync function rateLimitMiddleware(req: any, res: any, next: any) {\n  const key = `user:${req.userId}`\n  \n  if (!(await apiLimiter.isAllowed(key))) {\n    const info = await apiLimiter.getInfo(key)\n    return res.status(429).json({\n      error: \"Rate limit exceeded\",\n      retryAfter: new Date(info.resetTime).toISOString(),\n      remaining: info.remaining\n    })\n  }\n  \n  next()\n}\n```\n\n### **7. Better Error Handling**\n\n```typescript\nimport { createRedisRateLimiter } from \"better-ratelimit\"\n\nconst limiter = createRedisRateLimiter(process.env.REDIS_URL, 100, \"1h\")\n\n// Utility function for consistent error responses\nfunction createRateLimitError(result: any) {\n  return {\n    error: \"Rate limit exceeded\",\n    retryAfter: new Date(result.resetTime).toISOString(),\n    remaining: result.remaining,\n    limit: result.limit,\n    resetTime: result.resetTime\n  }\n}\n\n// In your API handler\nasync function handleApiRequest(userId: string) {\n  const result = await limiter.check(`user:${userId}`)\n  \n  if (!result.allowed) {\n    return createRateLimitError(result)\n  }\n  \n  // Process request...\n  return { \n    success: true, \n    remaining: result.remaining,\n    resetTime: new Date(result.resetTime).toISOString()\n  }\n}\n```\n\n### **With Elysia**\n\n```typescript\nimport { Elysia } from \"elysia\"\nimport { withRateLimiter } from \"better-ratelimit\"\n\nconst app = new Elysia()\n  .use(withRateLimiter({\n    key: ctx =\u003e ctx.ip,\n    limit: 100,\n    duration: \"1m\",\n    strategy: \"fixed-window\",\n    headers: {\n      enabled: true,\n      prefix: \"X-RateLimit\"\n    },\n    response: {\n      status: 429,\n      message: \"Too Many Requests\"\n    }\n  }))\n  .get(\"/api/data\", () =\u003e ({ data: \"...\" }))\n  .listen(3000)\n```\n\n### **With Hono**\n\n```typescript\nimport { Hono } from \"hono\"\nimport { withHonoRateLimiter } from \"better-ratelimit\"\n\nconst app = new Hono()\n\napp.use(withHonoRateLimiter({\n  key: ctx =\u003e ctx.req.header(\"x-user-id\") || \"anonymous\",\n  limit: 100,\n  duration: \"1m\",\n  strategy: \"sliding-window\",\n  headers: {\n    enabled: true,\n    prefix: \"X-RateLimit\"\n  },\n  response: {\n    status: 429,\n    message: \"Too Many Requests\"\n  }\n}))\n\napp.get(\"/api/data\", (c) =\u003e c.json({ data: \"...\" }))\n```\n\n### **Custom Key Generation**\n\n```typescript\nimport { getIPKey } from \"better-ratelimit\"\n\nconst result = await limiter.check({\n  key: getIPKey(ctx), // Handles Cloudflare, AWS, Vercel, etc.\n  limit: 50,\n  duration: \"5m\"\n})\n```\n\n## 🎯 **Features**\n\n### **Multiple Storage Backends**\n- **Memory** - Fast, in-memory storage for development\n- **Redis** - Production-ready with Dragonfly/Valkey support\n- **ClickHouse** - Analytics and historical data\n- **BunKV** - Edge function storage (coming soon)\n\n### **Rate Limiting Strategies**\n- **Fixed Window** - Simple, predictable limits\n- **Sliding Window** - Smooth rate limiting\n- **Approximated Sliding Window** - Efficient sliding with sub-windows\n\n### **Framework Integrations**\n- **Elysia** - First-class support\n- **Hono** - Coming soon\n- **Express** - Coming soon\n- **Edge Functions** - Coming soon\n\n### **Observability**\n- **Structured logging** - Every decision logged\n- **Performance metrics** - Response times, throughput\n- **Analytics ready** - Export to ClickHouse, Prometheus, etc.\n\n## 🏗️ **API Reference**\n\n### **Improved API Design**\n\nThe API is designed to be **intuitive and practical**:\n\n```typescript\nimport { RateLimiter } from \"better-ratelimit\"\n\n// ✅ Configure once, use many times\nconst limiter = new RateLimiter(store, {\n  limit: 100,\n  duration: \"1m\",\n  strategy: \"fixed-window\"\n})\n\n// ✅ Simple check - just pass the key\nconst result = await limiter.check(\"user:123\")\n\n// ✅ Helper methods for common patterns\nif (await limiter.isAllowed(\"user:123\")) {\n  // Process request\n}\n\nconst remaining = await limiter.getRemaining(\"user:123\")\nconst info = await limiter.getInfo(\"user:123\")\n```\n\n### **Why This Design?**\n\n- **🎯 Configure Once**: Set limits, duration, strategy at initialization\n- **🚀 Simple Usage**: Just pass the key to check\n- **🛠️ Helper Methods**: Common patterns like `isAllowed()`, `getRemaining()`\n- **📊 Rich Results**: Full information about rate limit status\n- **🔄 Consistent**: Same behavior across all checks\n\n### **RateLimiter**\n\n```typescript\nimport { RateLimiter } from \"better-ratelimit\"\n\nconst limiter = new RateLimiter(store, {\n  limit: 100,\n  duration: \"1m\",\n  strategy: \"fixed-window\"\n})\n\n// Check rate limit\nconst result = await limiter.check(\"user:123\")\n\n// Result\ninterface RateLimitResult {\n  allowed: boolean\n  remaining: number\n  resetTime: number\n  limit: number\n  key: string\n  metadata?: Record\u003cstring, unknown\u003e\n}\n```\n\n### **Storage Adapters**\n\n```typescript\n// Memory (default)\nimport { MemoryStore } from \"better-ratelimit\"\nconst store = new MemoryStore({ maxSize: 1000 })\n\n// Redis\nimport { RedisAdapter } from \"better-ratelimit\"\nconst store = new RedisAdapter({ \n  url: \"redis://localhost:6379\",\n  prefix: \"ratelimit\"\n})\n```\n\n### **Framework Plugins**\n\n```typescript\n// Elysia\nimport { withRateLimiter } from \"better-ratelimit\"\n\napp.use(withRateLimiter({\n  key: ctx =\u003e ctx.ip,\n  limit: 100,\n  duration: \"1m\",\n  strategy: \"fixed-window\",\n  onLimit: (ctx, result) =\u003e {\n    // Custom handling\n  }\n}))\n```\n\n## 🚀 **Quick Start**\n\n### Installation\n\n```bash\nbun add better-ratelimit-core\nbun add better-ratelimit-adapter-redis\nbun add better-ratelimit-plugin-elysia\n```\n\n### Basic Usage\n\n```typescript\nimport { Elysia } from \"elysia\"\nimport { withRateLimiter } from \"better-ratelimit-plugin-elysia\"\nimport { RedisLayer } from \"better-ratelimit-adapter-redis\"\n\nconst app = new Elysia()\n  .use(withRateLimiter({\n    key: ctx =\u003e ctx.ip,\n    limit: 100,\n    duration: \"1m\",\n    storage: RedisLayer\n  }))\n  .get(\"/\", () =\u003e \"Hello World!\")\n  .listen(3000)\n```\n\n### Advanced Usage\n\n```typescript\nimport { Effect, Layer } from \"effect\"\nimport { RateLimitCore } from \"better-ratelimit-core\"\nimport { RedisLayer } from \"better-ratelimit-adapter-redis\"\nimport { DatabuddyLayer } from \"better-ratelimit-observability-databuddy\"\n\nconst program = Effect.gen(function* (_) {\n  const rateLimiter = yield* _(RateLimitCore)\n  \n  const result = yield* _(\n    rateLimiter.check({\n      key: \"user:123\",\n      limit: 50,\n      duration: \"1h\"\n    })\n  )\n  \n  return result\n})\n\nconst runtime = Layer.provide(\n  Layer.merge(RateLimitCore, RedisLayer),\n  Layer.provide(DatabuddyLayer, program)\n)\n\nconst result = await Effect.runPromise(runtime)\n```\n\n## 🔧 **Configuration**\n\n### Rate Limiting Strategies\n\n```typescript\n// Token Bucket (default)\n{\n  strategy: \"token-bucket\",\n  limit: 100,\n  duration: \"1m\",\n  burst: 10\n}\n\n// Sliding Window\n{\n  strategy: \"sliding-window\", \n  limit: 100,\n  duration: \"1m\"\n}\n\n// Fixed Window\n{\n  strategy: \"fixed-window\",\n  limit: 100,\n  duration: \"1m\"\n}\n```\n\n### Storage Options\n\n```typescript\n// Redis\nimport { RedisLayer } from \"better-ratelimit-adapter-redis\"\n\n// ClickHouse for analytics\nimport { ClickHouseLayer } from \"better-ratelimit-adapter-clickhouse\"\n\n// Memory for testing\nimport { MemoryLayer } from \"better-ratelimit-adapter-memory\"\n\n// BunKV for edge\nimport { BunKVLayer } from \"better-ratelimit-adapter-bunkv\"\n```\n\n### Observability\n\n```typescript\n// Auto-log to Databuddy\nimport { DatabuddyLayer } from \"better-ratelimit-observability-databuddy\"\n\n// Console logging\nimport { ConsoleLayer } from \"better-ratelimit-observability-console\"\n\n// Custom observability\nimport { CustomLayer } from \"better-ratelimit-observability-custom\"\n```\n\n## 🧪 **Testing**\n\n```typescript\nimport { describe, it, expect } from \"bun:test\"\nimport { RateLimiter, MemoryStore } from \"better-ratelimit\"\n\ndescribe(\"Rate Limiting\", () =\u003e {\n  it(\"should limit requests correctly\", async () =\u003e {\n    const limiter = new RateLimiter(new MemoryStore())\n    \n    // First request - allowed\n    const result1 = await limiter.check({\n      key: \"test:user\",\n      limit: 2,\n      duration: \"1m\"\n    })\n    expect(result1.allowed).toBe(true)\n    expect(result1.remaining).toBe(1)\n    \n    // Second request - allowed\n    const result2 = await limiter.check({\n      key: \"test:user\",\n      limit: 2,\n      duration: \"1m\"\n    })\n    expect(result2.allowed).toBe(true)\n    expect(result2.remaining).toBe(0)\n    \n    // Third request - blocked\n    const result3 = await limiter.check({\n      key: \"test:user\",\n      limit: 2,\n      duration: \"1m\"\n    })\n    expect(result3.allowed).toBe(false)\n    expect(result3.remaining).toBe(0)\n  })\n})\n```\n\n## 🐳 **Development**\n\n### **Start Databases**\n\n```bash\n# Start Redis, Dragonfly, Valkey, ClickHouse\ndocker-compose up -d\n\n# Test all databases\nbun test src/adapters/redis/redis.test.ts\n```\n\n### **Environment Variables**\n\n```bash\n# Copy example\ncp env.example .env\n\n# Configure databases\nREDIS_URL=redis://localhost:6379\nDRAGONFLY_URL=redis://localhost:6380\nVALKEY_URL=redis://localhost:6381\n```\n\n## 📊 **Observability**\n\nEvery rate limit decision is automatically logged with structured data:\n\n```typescript\ninterface RateLimitEvent {\n  timestamp: string\n  key: string\n  allowed: boolean\n  limit: number\n  remaining: number\n  resetTime: string\n  strategy: string\n  responseTime: number\n  metadata?: Record\u003cstring, unknown\u003e\n}\n```\n\n## 🌍 **Framework Support**\n\n- **Elysia** ✅ - First-class support\n- **Hono** 🚧 - Coming soon\n- **Express** 🚧 - Coming soon\n- **Edge Functions** 🚧 - Coming soon\n\n## 🤝 **Contributing**\n\nBuilt with **[Bun](https://bun.sh)**, **[Effect](https://effect.website/)**, and **[TypeScript](https://www.typescriptlang.org/)**.\n\n```bash\n# Install dependencies\nbun install\n\n# Run tests\nbun test\n\n# Start development databases\ndocker-compose up -d\n```\n\n## 📄 **License**\n\nMIT License - see [LICENSE](LICENSE) for details.\n\n---\n\n**better-ratelimit** - Making rate limiting composable, observable, and developer-friendly.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fdatabuddy-analytics%2Fbetter-ratelimit","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fdatabuddy-analytics%2Fbetter-ratelimit","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fdatabuddy-analytics%2Fbetter-ratelimit/lists"}