hono-rate-shield
v0.1.0
Published
Lightweight, type-safe rate limiting middleware for Cloudflare Workers + Hono. Fixed window, sliding window, and token bucket with KV, Durable Objects, and in-memory backends.
Maintainers
Readme
hono-rate-shield
Type-safe, zero-dependency rate limiting middleware for Hono + Cloudflare Workers.
Fixed window, sliding window, and token bucket algorithms with KV, Durable Objects, and in-memory backends -- all built on Web Crypto API with no external runtime dependencies.
npm install hono-rate-shieldWhy hono-rate-shield?
- Zero runtime dependencies -- only
honoas a peer dependency - Built for Workers -- runs natively on Cloudflare Workers, no Node.js polyfills
- Three algorithms -- fixed window, sliding window, token bucket
- Three backends -- Cloudflare KV (distributed), Durable Objects (precise), in-memory (dev/test)
- Type-safe -- full TypeScript with strict mode
- Tree-shakeable -- import only what you need (
/kv,/memory,/durable-object) - Tiny -- under 10KB gzipped (core)
- RFC 7807 errors -- standards-compliant problem detail responses
- Composable -- works alongside hono-auth-guard
Quick Start
Basic Rate Limiting (KV)
import { Hono } from 'hono'
import { rateLimit } from 'hono-rate-shield'
import { kvStore } from 'hono-rate-shield/kv'
type Env = { Bindings: { RATE_LIMIT_KV: KVNamespace } }
const app = new Hono<Env>()
app.use('/api/*', (c, next) => {
const limiter = rateLimit({
limit: 100,
window: '1m',
store: kvStore(c.env.RATE_LIMIT_KV),
})
return limiter(c, next)
})
app.get('/api/hello', (c) => c.json({ message: 'Hello!' }))
export default appIn-Memory (Development)
import { rateLimit } from 'hono-rate-shield'
import { memoryStore } from 'hono-rate-shield/memory'
app.use('/api/*', rateLimit({
limit: 100,
window: '1m',
store: memoryStore(),
}))Sliding Window
import { slidingWindowStore } from 'hono-rate-shield/memory'
app.use('/api/*', rateLimit({
limit: 100,
window: '1m',
store: slidingWindowStore(),
}))Token Bucket
import { tokenBucketStore } from 'hono-rate-shield/memory'
app.use('/api/*', rateLimit({
limit: 100, // bucket capacity
window: '1m', // refill period
store: tokenBucketStore(),
}))Durable Objects (Precise)
import { rateLimit } from 'hono-rate-shield'
import { durableObjectStore, RateLimiterDO } from 'hono-rate-shield/durable-object'
app.use('/api/*', (c, next) => {
const limiter = rateLimit({
limit: 100,
window: '1m',
store: durableObjectStore(c.env.RATE_LIMITER),
})
return limiter(c, next)
})
// Export the DO class for wrangler.toml
export { RateLimiterDO }Per-User Rate Limiting with hono-auth-guard
import { jwtGuard } from 'hono-auth-guard/jwt'
import { rateLimit } from 'hono-rate-shield'
import { kvStore } from 'hono-rate-shield/kv'
// Auth guard runs first, then rate limit by user
app.use('/api/*', jwtGuard({ secret: env.JWT_SECRET }))
app.use('/api/*', rateLimit({
limit: 1000,
window: '1h',
store: kvStore(env.RATE_LIMIT_KV),
keyGenerator: (c) => c.get('auth').subject,
}))Multiple Rate Limits
Stack burst + sustained limits:
import { memoryStore } from 'hono-rate-shield/memory'
import { kvStore } from 'hono-rate-shield/kv'
// Short burst limit (per-isolate)
app.use('/api/*', rateLimit({
limit: 10,
window: '1s',
store: memoryStore(),
keyPrefix: 'burst',
}))
// Sustained limit (distributed)
app.use('/api/*', (c, next) => {
const limiter = rateLimit({
limit: 1000,
window: '1h',
store: kvStore(c.env.RATE_LIMIT_KV),
keyPrefix: 'sustained',
})
return limiter(c, next)
})Skip Rules
Bypass rate limiting for internal requests:
app.use('/api/*', rateLimit({
limit: 100,
window: '1m',
store: memoryStore(),
skip: (c) => c.req.header('X-Internal-Token') === env.INTERNAL_TOKEN,
}))Custom 429 Response
app.use('/api/*', rateLimit({
limit: 100,
window: '1m',
store: memoryStore(),
handler: (c, info) => {
return c.json({
error: 'Too many requests',
retryIn: info.resetTime - Math.floor(Date.now() / 1000),
}, 429)
},
}))API Reference
rateLimit(options)
| Option | Type | Default | Description |
|--------|------|---------|-------------|
| limit | number | (required) | Max requests per window |
| window | number \| string | (required) | Duration: seconds or "30s", "1m", "1h", "1d" |
| algorithm | string | 'fixed-window' | 'fixed-window', 'sliding-window', 'token-bucket' |
| store | RateLimitStore | (required) | Storage backend |
| keyGenerator | (c) => string | CF-Connecting-IP | Extract rate limit key |
| handler | (c, info) => Response | RFC 7807 JSON | Custom 429 response |
| skip | (c) => boolean | -- | Bypass rate limiting |
| headers | boolean | true | Add rate limit headers |
| keyPrefix | string | 'rl' | Key prefix in store |
Response Headers
X-RateLimit-Limit: 100
X-RateLimit-Remaining: 42
X-RateLimit-Reset: 1719561600
Retry-After: 30 (only on 429 responses)Error Response (RFC 7807)
{
"type": "https://hono-rate-shield.dev/errors/rate-limit-exceeded",
"title": "Rate Limit Exceeded",
"status": 429,
"detail": "You have exceeded the rate limit of 100 requests per 60 seconds.",
"instance": "/api/users"
}Store Interface
Implement RateLimitStore for custom backends:
interface RateLimitStore {
increment(key: string, window: number, limit: number): Promise<RateLimitInfo>
reset?(key: string): Promise<void>
}Stores Comparison
| Store | Consistency | Distribution | Cost | Best For |
|-------|------------|-------------|------|----------|
| memoryStore() | Strong (per-isolate) | None | Free | Development, testing |
| kvStore(kv) | Eventual (~60s) | Global | Low | Soft rate limits |
| durableObjectStore(ns) | Strong | Global | Higher | Precise enforcement |
Testing
import { createTestStore, resetAll } from 'hono-rate-shield/testing'
const store = createTestStore()
beforeEach(async () => {
await resetAll(store)
})
test('rate limits after 100 requests', async () => {
for (let i = 0; i < 100; i++) {
await store.increment('test-key', 60, 100)
}
const info = await store.increment('test-key', 60, 100)
expect(info.isLimited).toBe(true)
})Requirements
- Runtime: Cloudflare Workers (V8 Isolate)
- Framework: Hono v4+
- Node.js: 20+ (development only)
Development
git clone https://github.com/ikeno-web/hono-rate-shield.git
cd hono-rate-shield
npm install
npm run dev # watch mode
npm run lint # biome check
npm run typecheck # tsc --noEmit
npm run test # vitest
npm run test:cov # with coverage
npm run build # ESM + CJS + DTSLicense
MIT -- ikeno-web
