Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings

databuddy-analytics/better-ratelimit

Open more actions menu

Folders and files

NameName
Last commit message
Last commit date

Latest commit

Β 

History

26 Commits
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 

Repository files navigation

better-ratelimit

A framework-agnostic, Effect-powered, observability-native rate limiter designed for real-world infrastructure.

Bun TypeScript Effect

πŸš€ Installation

bun add better-ratelimit

πŸ“š Real-World Examples

1. API Rate Limiting

import { createRedisRateLimiter } from "better-ratelimit"

// API endpoint rate limiting: 100 requests per hour per user
const apiLimiter = createRedisRateLimiter(
  process.env.REDIS_URL,
  100,
  "1h",
  { 
    prefix: "api:ratelimit",
    strategy: "sliding-window"
  }
)

// In your API route handler
async function handleApiRequest(userId: string) {
  const result = await apiLimiter.check(`user:${userId}`)
  
  if (!result.allowed) {
    return {
      error: "Rate limit exceeded",
      retryAfter: new Date(result.resetTime).toISOString(),
      remaining: result.remaining
    }
  }
  
  // Process the request...
  return { success: true, remaining: result.remaining }
}

2. Login Attempt Protection

import { createRedisRateLimiter } from "better-ratelimit"

// Login protection: 5 attempts per 15 minutes per IP
const loginLimiter = createRedisRateLimiter(
  process.env.REDIS_URL,
  5,
  "15m",
  { 
    prefix: "login:ratelimit",
    strategy: "fixed-window"
  }
)

// In your login endpoint
async function handleLogin(ip: string, email: string) {
  const result = await loginLimiter.check(`ip:${ip}`)
  
  if (!result.allowed) {
    return {
      error: "Too many login attempts",
      retryAfter: new Date(result.resetTime).toISOString()
    }
  }
  
  // Attempt login...
  return { success: true }
}

3. File Upload Limiting

import { createMemoryRateLimiter } from "better-ratelimit"

// File upload: 10 files per day per user
const uploadLimiter = createMemoryRateLimiter(10, "24h")

// In your upload handler
async function handleFileUpload(userId: string) {
  const result = await uploadLimiter.check(`upload:${userId}`)
  
  if (!result.allowed) {
    return {
      error: "Upload limit reached",
      remaining: result.remaining,
      resetDate: new Date(result.resetTime).toLocaleDateString()
    }
  }
  
  // Process upload...
  return { success: true, remaining: result.remaining }
}

4. Webhook Rate Limiting

import { createRedisRateLimiter } from "better-ratelimit"

// Webhook delivery: 1000 calls per hour per webhook
const webhookLimiter = createRedisRateLimiter(
  process.env.REDIS_URL,
  1000,
  "1h",
  { 
    prefix: "webhook:ratelimit",
    strategy: "approximated-sliding-window"
  }
)

// In your webhook sender
async function sendWebhook(webhookId: string, payload: any) {
  const result = await webhookLimiter.check(`webhook:${webhookId}`)
  
  if (!result.allowed) {
    console.log(`Webhook ${webhookId} rate limited, will retry later`)
    return { queued: true }
  }
  
  // Send webhook...
  return { sent: true, remaining: result.remaining }
}

5. Helper Methods & Common Patterns

import { createRedisRateLimiter } from "better-ratelimit"

const limiter = createRedisRateLimiter(process.env.REDIS_URL, 100, "1h")

// Simple boolean check
if (await limiter.isAllowed("user:123")) {
  // Process request
}

// Get remaining requests
const remaining = await limiter.getRemaining("user:123")
console.log(`${remaining} requests remaining`)

// Get reset time
const resetTime = await limiter.getResetTime("user:123")
console.log(`Resets at ${new Date(resetTime).toISOString()}`)

// Get all info at once
const info = await limiter.getInfo("user:123")
if (!info.allowed) {
  return {
    error: "Rate limit exceeded",
    remaining: info.remaining,
    retryAfter: new Date(info.resetTime).toISOString()
  }
}

6. Middleware Pattern

import { createRedisRateLimiter } from "better-ratelimit"

const apiLimiter = createRedisRateLimiter(process.env.REDIS_URL, 100, "1h")

// Express-style middleware
async function rateLimitMiddleware(req: any, res: any, next: any) {
  const key = `user:${req.userId}`
  
  if (!(await apiLimiter.isAllowed(key))) {
    const info = await apiLimiter.getInfo(key)
    return res.status(429).json({
      error: "Rate limit exceeded",
      retryAfter: new Date(info.resetTime).toISOString(),
      remaining: info.remaining
    })
  }
  
  next()
}

7. Better Error Handling

import { createRedisRateLimiter } from "better-ratelimit"

const limiter = createRedisRateLimiter(process.env.REDIS_URL, 100, "1h")

// Utility function for consistent error responses
function createRateLimitError(result: any) {
  return {
    error: "Rate limit exceeded",
    retryAfter: new Date(result.resetTime).toISOString(),
    remaining: result.remaining,
    limit: result.limit,
    resetTime: result.resetTime
  }
}

// In your API handler
async function handleApiRequest(userId: string) {
  const result = await limiter.check(`user:${userId}`)
  
  if (!result.allowed) {
    return createRateLimitError(result)
  }
  
  // Process request...
  return { 
    success: true, 
    remaining: result.remaining,
    resetTime: new Date(result.resetTime).toISOString()
  }
}

With Elysia

import { Elysia } from "elysia"
import { withRateLimiter } from "better-ratelimit"

const app = new Elysia()
  .use(withRateLimiter({
    key: ctx => ctx.ip,
    limit: 100,
    duration: "1m",
    strategy: "fixed-window",
    headers: {
      enabled: true,
      prefix: "X-RateLimit"
    },
    response: {
      status: 429,
      message: "Too Many Requests"
    }
  }))
  .get("/api/data", () => ({ data: "..." }))
  .listen(3000)

With Hono

import { Hono } from "hono"
import { withHonoRateLimiter } from "better-ratelimit"

const app = new Hono()

app.use(withHonoRateLimiter({
  key: ctx => ctx.req.header("x-user-id") || "anonymous",
  limit: 100,
  duration: "1m",
  strategy: "sliding-window",
  headers: {
    enabled: true,
    prefix: "X-RateLimit"
  },
  response: {
    status: 429,
    message: "Too Many Requests"
  }
}))

app.get("/api/data", (c) => c.json({ data: "..." }))

Custom Key Generation

import { getIPKey } from "better-ratelimit"

const result = await limiter.check({
  key: getIPKey(ctx), // Handles Cloudflare, AWS, Vercel, etc.
  limit: 50,
  duration: "5m"
})

🎯 Features

Multiple Storage Backends

  • Memory - Fast, in-memory storage for development
  • Redis - Production-ready with Dragonfly/Valkey support
  • ClickHouse - Analytics and historical data
  • BunKV - Edge function storage (coming soon)

Rate Limiting Strategies

  • Fixed Window - Simple, predictable limits
  • Sliding Window - Smooth rate limiting
  • Approximated Sliding Window - Efficient sliding with sub-windows

Framework Integrations

  • Elysia - First-class support
  • Hono - Coming soon
  • Express - Coming soon
  • Edge Functions - Coming soon

Observability

  • Structured logging - Every decision logged
  • Performance metrics - Response times, throughput
  • Analytics ready - Export to ClickHouse, Prometheus, etc.

πŸ—οΈ API Reference

Improved API Design

The API is designed to be intuitive and practical:

import { RateLimiter } from "better-ratelimit"

// βœ… Configure once, use many times
const limiter = new RateLimiter(store, {
  limit: 100,
  duration: "1m",
  strategy: "fixed-window"
})

// βœ… Simple check - just pass the key
const result = await limiter.check("user:123")

// βœ… Helper methods for common patterns
if (await limiter.isAllowed("user:123")) {
  // Process request
}

const remaining = await limiter.getRemaining("user:123")
const info = await limiter.getInfo("user:123")

Why This Design?

  • 🎯 Configure Once: Set limits, duration, strategy at initialization
  • πŸš€ Simple Usage: Just pass the key to check
  • πŸ› οΈ Helper Methods: Common patterns like isAllowed(), getRemaining()
  • πŸ“Š Rich Results: Full information about rate limit status
  • πŸ”„ Consistent: Same behavior across all checks

RateLimiter

import { RateLimiter } from "better-ratelimit"

const limiter = new RateLimiter(store, {
  limit: 100,
  duration: "1m",
  strategy: "fixed-window"
})

// Check rate limit
const result = await limiter.check("user:123")

// Result
interface RateLimitResult {
  allowed: boolean
  remaining: number
  resetTime: number
  limit: number
  key: string
  metadata?: Record<string, unknown>
}

Storage Adapters

// Memory (default)
import { MemoryStore } from "better-ratelimit"
const store = new MemoryStore({ maxSize: 1000 })

// Redis
import { RedisAdapter } from "better-ratelimit"
const store = new RedisAdapter({ 
  url: "redis://localhost:6379",
  prefix: "ratelimit"
})

Framework Plugins

// Elysia
import { withRateLimiter } from "better-ratelimit"

app.use(withRateLimiter({
  key: ctx => ctx.ip,
  limit: 100,
  duration: "1m",
  strategy: "fixed-window",
  onLimit: (ctx, result) => {
    // Custom handling
  }
}))

πŸš€ Quick Start

Installation

bun add better-ratelimit-core
bun add better-ratelimit-adapter-redis
bun add better-ratelimit-plugin-elysia

Basic Usage

import { Elysia } from "elysia"
import { withRateLimiter } from "better-ratelimit-plugin-elysia"
import { RedisLayer } from "better-ratelimit-adapter-redis"

const app = new Elysia()
  .use(withRateLimiter({
    key: ctx => ctx.ip,
    limit: 100,
    duration: "1m",
    storage: RedisLayer
  }))
  .get("/", () => "Hello World!")
  .listen(3000)

Advanced Usage

import { Effect, Layer } from "effect"
import { RateLimitCore } from "better-ratelimit-core"
import { RedisLayer } from "better-ratelimit-adapter-redis"
import { DatabuddyLayer } from "better-ratelimit-observability-databuddy"

const program = Effect.gen(function* (_) {
  const rateLimiter = yield* _(RateLimitCore)
  
  const result = yield* _(
    rateLimiter.check({
      key: "user:123",
      limit: 50,
      duration: "1h"
    })
  )
  
  return result
})

const runtime = Layer.provide(
  Layer.merge(RateLimitCore, RedisLayer),
  Layer.provide(DatabuddyLayer, program)
)

const result = await Effect.runPromise(runtime)

πŸ”§ Configuration

Rate Limiting Strategies

// Token Bucket (default)
{
  strategy: "token-bucket",
  limit: 100,
  duration: "1m",
  burst: 10
}

// Sliding Window
{
  strategy: "sliding-window", 
  limit: 100,
  duration: "1m"
}

// Fixed Window
{
  strategy: "fixed-window",
  limit: 100,
  duration: "1m"
}

Storage Options

// Redis
import { RedisLayer } from "better-ratelimit-adapter-redis"

// ClickHouse for analytics
import { ClickHouseLayer } from "better-ratelimit-adapter-clickhouse"

// Memory for testing
import { MemoryLayer } from "better-ratelimit-adapter-memory"

// BunKV for edge
import { BunKVLayer } from "better-ratelimit-adapter-bunkv"

Observability

// Auto-log to Databuddy
import { DatabuddyLayer } from "better-ratelimit-observability-databuddy"

// Console logging
import { ConsoleLayer } from "better-ratelimit-observability-console"

// Custom observability
import { CustomLayer } from "better-ratelimit-observability-custom"

πŸ§ͺ Testing

import { describe, it, expect } from "bun:test"
import { RateLimiter, MemoryStore } from "better-ratelimit"

describe("Rate Limiting", () => {
  it("should limit requests correctly", async () => {
    const limiter = new RateLimiter(new MemoryStore())
    
    // First request - allowed
    const result1 = await limiter.check({
      key: "test:user",
      limit: 2,
      duration: "1m"
    })
    expect(result1.allowed).toBe(true)
    expect(result1.remaining).toBe(1)
    
    // Second request - allowed
    const result2 = await limiter.check({
      key: "test:user",
      limit: 2,
      duration: "1m"
    })
    expect(result2.allowed).toBe(true)
    expect(result2.remaining).toBe(0)
    
    // Third request - blocked
    const result3 = await limiter.check({
      key: "test:user",
      limit: 2,
      duration: "1m"
    })
    expect(result3.allowed).toBe(false)
    expect(result3.remaining).toBe(0)
  })
})

🐳 Development

Start Databases

# Start Redis, Dragonfly, Valkey, ClickHouse
docker-compose up -d

# Test all databases
bun test src/adapters/redis/redis.test.ts

Environment Variables

# Copy example
cp env.example .env

# Configure databases
REDIS_URL=redis://localhost:6379
DRAGONFLY_URL=redis://localhost:6380
VALKEY_URL=redis://localhost:6381

πŸ“Š Observability

Every rate limit decision is automatically logged with structured data:

interface RateLimitEvent {
  timestamp: string
  key: string
  allowed: boolean
  limit: number
  remaining: number
  resetTime: string
  strategy: string
  responseTime: number
  metadata?: Record<string, unknown>
}

🌍 Framework Support

  • Elysia βœ… - First-class support
  • Hono 🚧 - Coming soon
  • Express 🚧 - Coming soon
  • Edge Functions 🚧 - Coming soon

🀝 Contributing

Built with Bun, Effect, and TypeScript.

# Install dependencies
bun install

# Run tests
bun test

# Start development databases
docker-compose up -d

πŸ“„ License

MIT License - see LICENSE for details.


better-ratelimit - Making rate limiting composable, observable, and developer-friendly.

Morty Proxy This is a proxified and sanitized view of the page, visit original site.