npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

gcra-throttle

v1.2.1

Published

High-performance rate limiting library with GCRA algorithm for Bun and Node.js

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 elysia

Quick 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_API

Blacklist 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_TAT

API 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 support
  • BunRedisAdapter - Native Bun.RedisClient wrapper
  • NodeRedisAdapter - npm redis package 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 до запуска скрипта. Рекомендуемые варианты:

  1. Один primary + replicas (single-slot, никакого sharding'а) — самый простой вариант для rate limiter'а.
  2. Использовать ThrottleIdentity без ip-поля (или строковый ключ без throttle:<ip>: префикса) — тогда IP-level keys резолвятся в sentinel и не попадают в разные slot'ы. Auto-blacklist по IP в этом случае не работает.
  3. Дождаться будущей версии где 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