gcra-throttle
v1.2.1
Published
High-performance rate limiting library with GCRA algorithm for Bun and Node.js
Maintainers
Readme
gcra-throttle
High-performance rate limiting library with GCRA algorithm for Bun and Node.js.
Features
- GCRA Algorithm - Generic Cell Rate Algorithm (ITU-T I.371 standard), more accurate than token bucket
- Multiple Adapters - Bun.RedisClient, npm
redis, or in-memory - Dragonfly-ready - все Lua-скрипты работают только с declared keys; CI гоняет тесты и против Redis, и против Dragonfly в строгом режиме (без
allow-undeclared-keys). Производные ключи сгруппированы hash tag'ом{<key>}:...для locality. IP-level ключи (blacklist:ip:<ip>,suspicious:ip:<ip>) намеренно в отдельном slot'е — для multi-node Redis Cluster см. "Cluster notes" ниже. - Zero Required Dependencies - redis and elysia are optional peer dependencies
- Elysia Middleware - First-class support for Elysia framework
- WebSocket Support - Built-in throttler for WebSocket connections and messages
- IP Detection - Smart client IP detection with proxy header support
- Blacklist Support - Global, per-key и auto-blacklist (конфигурируемый)
- Configurable error handling - fail-open по умолчанию, custom logger и onBackendError hook для метрик
- Smoke EVALSHA on connect - ловит проблемы Lua-совместимости backend'а до первого пользовательского запроса
- TypeScript - Full type safety with TypeScript declarations
Installation
# Bun
bun add gcra-throttle
# npm
npm install gcra-throttle
# With Redis support (optional)
bun add gcra-throttle redis
# With Elysia support (optional)
bun add gcra-throttle elysiaQuick Start
Basic Usage (Memory Adapter)
import { createThrottler, MemoryAdapter, ThrottlePresets } from 'gcra-throttle'
const adapter = new MemoryAdapter()
await adapter.connect()
const throttler = await createThrottler({ adapter })
// Рекомендуемый structured API — без regex-парсинга, IPv6-safe
const result = await throttler.throttle(
{ ip: '192.168.1.1', userId: 'u123' },
ThrottlePresets.GENERAL_API
)
// Или старый строковый ключ (для обратной совместимости)
// const result = await throttler.throttle('throttle:192.168.1.1:user:u123', ...)
if (result.limited) {
console.log(`Rate limited! Retry after ${result.retryAfter} seconds`)
} else {
console.log(`Allowed. ${result.remaining} requests remaining`)
}
await throttler.close()MemoryAdapter реализует ту же логику что и Lua-путь на Redis/Dragonfly (blacklist checks, suspicious mode, auto-blacklist). Можно тестировать локально с memory и быть уверенным что поведение на проде совпадёт — parity закреплён tests/adapter-parity.test.ts.
With Bun.RedisClient
import { createThrottler, BunRedisAdapter, ThrottlePresets } from 'gcra-throttle'
const adapter = new BunRedisAdapter('redis://localhost:6379')
await adapter.connect()
const throttler = await createThrottler({ adapter })
const result = await throttler.throttle('api:user:123', {
maxBurst: 10, // Allow burst of 10 requests
countPerPeriod: 60, // 60 requests per period
period: 60 // Period in seconds
})With npm redis (Node.js compatible)
import { createClient } from 'redis'
import { createThrottler, NodeRedisAdapter, ThrottlePresets } from 'gcra-throttle'
// Create redis client
const redis = createClient({ url: 'redis://localhost:6379' })
await redis.connect()
// Wrap with adapter
const adapter = new NodeRedisAdapter(redis)
await adapter.connect()
const throttler = await createThrottler({ adapter })
const result = await throttler.throttle('api:user:123', ThrottlePresets.STRICT_API)Elysia Middleware
Option 1: Direct import (requires elysia)
import { Elysia } from 'elysia'
import { createThrottleMiddleware } from 'gcra-throttle/elysia'
import { createThrottler, MemoryAdapter, ThrottlePresets } from 'gcra-throttle'
const adapter = new MemoryAdapter()
await adapter.connect()
const throttler = await createThrottler({ adapter })
const app = new Elysia()
.use(createThrottleMiddleware(throttler.service, {
...ThrottlePresets.GENERAL_API,
includeUserAgent: true,
includePath: true
}))
.get('/', () => 'Hello World')
.listen(3000)Option 2: Dynamic import (lazy loading)
import { Elysia } from 'elysia'
import { createThrottler, MemoryAdapter, loadElysiaMiddleware } from 'gcra-throttle'
const adapter = new MemoryAdapter()
await adapter.connect()
const throttler = await createThrottler({ adapter })
// Lazy load elysia middleware
const { createThrottleMiddleware } = await loadElysiaMiddleware()
const app = new Elysia()
.use(createThrottleMiddleware(throttler.service, {
maxBurst: 20,
countPerPeriod: 100,
period: 60
}))
.get('/api', () => ({ status: 'ok' }))
.listen(3000)WebSocket Throttling
import { createThrottler, MemoryAdapter, ThrottlePresets } from 'gcra-throttle'
const adapter = new MemoryAdapter()
await adapter.connect()
const throttler = await createThrottler({ adapter })
const wsThrottler = throttler.createWsThrottler()
// Throttle connection attempts
const canConnect = await wsThrottler.throttleConnection({
ip: '192.168.1.1',
maxBurst: 5,
countPerPeriod: 10,
period: 60
})
// Throttle messages
const canSendMessage = await wsThrottler.throttleMessage({
ip: '192.168.1.1',
userId: 'user123',
maxBurst: 10,
countPerPeriod: 30,
period: 60
})Presets
Built-in presets for common use cases:
import { ThrottlePresets } from 'gcra-throttle'
// General API - 60 req/min with burst of 20
ThrottlePresets.GENERAL_API
// Strict API - 20 req/min with burst of 5
ThrottlePresets.STRICT_API
// Login protection - 5 req/min, no burst
ThrottlePresets.LOGIN_PROTECTION
// Signup protection - 3 req/hour, no burst
ThrottlePresets.SIGNUP_PROTECTION
// WebSocket connections - 10 conn/min with burst of 5
ThrottlePresets.WEBSOCKET_CONNECTION
// WebSocket messages - 120 msg/min with burst of 30
ThrottlePresets.WEBSOCKET_MESSAGE
// Microservice - 1000 req/min with burst of 100
ThrottlePresets.MICROSERVICE
// Public API - 100 req/min with burst of 30
ThrottlePresets.PUBLIC_APIBlacklist Management
const throttler = await createThrottler({ adapter })
// Add to blacklist (default TTL: 1 hour)
await throttler.addToBlacklist('ip:192.168.1.100')
// Add with custom TTL (24 hours)
await throttler.addToBlacklist('ip:192.168.1.100', 86400)
// Check if blacklisted
const isBlocked = await throttler.isBlacklisted('ip:192.168.1.100')
// Remove from blacklist
await throttler.removeFromBlacklist('ip:192.168.1.100')Key Generation
const throttler = await createThrottler({ adapter })
// Generate consistent keys from request parameters
const key = throttler.generateKey({
ip: '192.168.1.1',
path: '/api/users',
method: 'POST',
userId: 'user123'
})
// Result: "192.168.1.1:/api/users:POST:user123"IP Detection
Smart IP detection with support for common proxy headers:
import { getClientIP, IP_HEADERS, deriveClientIP } from 'gcra-throttle'
// Get IP from request
const ip = getClientIP(request)
// Supported headers (in priority order):
// - cf-connecting-ip (Cloudflare)
// - x-forwarded-for
// - x-real-ip
// - true-client-ip
// - x-client-ip
// - fastly-client-ip
// - and more...GCRA Algorithm
This library uses the Generic Cell Rate Algorithm (GCRA), also known as the "virtual scheduling" algorithm. It's defined in ITU-T I.371 and provides:
- Single state value - Only stores TAT (Theoretical Arrival Time) per key
- Mathematically precise - No floating-point accumulation errors
- Smooth rate limiting - Prevents burst clustering at period boundaries
- Efficient - Single GET + SET operation per check
How it works
TAT (Theoretical Arrival Time) = time when bucket will be empty
T = emission interval = period / countPerPeriod
τ = tolerance = T × maxBurst
For each request:
new_TAT = max(now, TAT) + T
if new_TAT - now > τ:
REJECT (request came too early)
else:
ALLOW and update TAT = new_TATAPI Reference
createThrottler(options)
Factory function to create a throttler instance.
interface ThrottleFactoryOptions {
adapter?: StorageAdapter // Injected adapter instance
adapterType?: 'memory' | 'bun' | 'node' // Auto-create adapter
redisUrl?: string // Redis URL for auto-created adapters
fallbackToMemory?: boolean // Fallback to memory if Redis fails
}ThrottleResult
interface ThrottleResult {
limited: boolean // true if request should be rejected
limit: number // Maximum requests allowed
remaining: number // Remaining requests in current window
retryAfter: number // Seconds until next request allowed (if limited)
resetAfter: number // Seconds until limit resets
}Adapters
All adapters implement the StorageAdapter interface:
MemoryAdapter- In-memory storage with TTL supportBunRedisAdapter- Native Bun.RedisClient wrapperNodeRedisAdapter- npmredispackage wrapper
Error handling & resiliency
По умолчанию адаптеры fail-open: если Redis/Dragonfly упал, запрос пропускается (limited=false). Это правильный выбор для user-facing rate limiter'а — лучше пропустить трафик, чем положить продукт из-за проблем с инфраструктурой.
import { BunRedisAdapter } from 'gcra-throttle'
const adapter = new BunRedisAdapter('redis://localhost:6379', {
// Logger (duck-typed, совместим с console/pino/logtape/winston)
logger: console,
// Политика при backend error: 'fail-open' (default), 'fail-closed',
// либо функция (err, key) => ThrottleResult
onError: 'fail-open',
// Callback для метрик (Prometheus counter и т.п.)
onBackendError: (err, key) => {
metrics.throttleBackendErrors.inc({ key_prefix: key.split(':')[0] })
},
// Auto-blacklist policy (дефолт: 10 нарушений/час → 24h ban).
// Для user-facing эндпоинтов может быть слишком агрессивным.
autoBlacklist: {
enabled: true,
threshold: 10,
ttl: 86400,
},
// Smoke EVALSHA при connect() — ловит Lua-несовместимость backend'а
// (default: true)
smokeTestOnConnect: true,
})Dragonfly compatibility
Библиотека тестируется в CI против двух backend'ов:
- Redis 8 (стандартный)
- Dragonfly в строгом режиме — без
--default_lua_flags=allow-undeclared-keys
Все Lua-скрипты обращаются только к declared keys (KEYS[1..N]), не конструируют имена внутри скрипта. Производные ключи (:tat, :activity, :violation, :blacklist, :suspicious) сгруппированы через hash tags {<key>}:.... См. src/scripts/throttle.lua для контракта ключей.
Cluster notes
На single-node Redis/Dragonfly — работает из коробки.
На multi-node Redis Cluster есть ограничение: throttle.lua декларирует, помимо 5 keys вида {<key>}:..., ещё 2 IP-level key'а (blacklist:ip:<ip>, suspicious:ip:<ip>), которые попадают в другой slot. Redis Cluster ответит CROSSSLOT до запуска скрипта. Рекомендуемые варианты:
- Один primary + replicas (single-slot, никакого sharding'а) — самый простой вариант для rate limiter'а.
- Использовать
ThrottleIdentityбезip-поля (или строковый ключ безthrottle:<ip>:префикса) — тогда IP-level keys резолвятся в sentinel и не попадают в разные slot'ы. Auto-blacklist по IP в этом случае не работает. - Дождаться будущей версии где IP-level проверки переедут за пределы Lua (один EXISTS до EVALSHA) — принято в roadmap.
Requirements
- Runtime: Bun 1.0+ or Node.js 18+
- Redis: 6.0+ / Dragonfly 1.x+
- Optional: elysia 1.0+ (for Elysia middleware)
License
MIT
