Ratelock

Integrations

Hono

Hono

Rate limiting middleware for Hono, powered by any RateLock engine.

  • Engine-agnostic: bring a @ratelock/local, @ratelock/redis or @ratelock/postgres limiter
  • Lazy initialization: pass a factory and the engine starts on the first matched request (cold-start friendly for edge/serverless)
  • Dual-family headers: RateLimit-* (RFC 9331) and X-RateLimit-*, configurable
  • Zero runtime dependency besides Hono itself

Installation

pnpm add @ratelock/hono @ratelock/redis

Quick start

import { Hono } from 'hono'
import { rateLimit } from '@ratelock/hono'
import { fixedWindow } from '@ratelock/redis'

const app = new Hono()

app.use(
    '/api/*',
    rateLimit({
        // Lazy factory: nothing initializes until the first request hits /api/*
        limiter: () => fixedWindow({ url: process.env.REDIS_URL!, limit: 100, windowMs: 60_000 }),
        limit: 100,
        keyGenerator: c => c.req.header('x-user-id') ?? 'anon',
    })
)

app.get('/api/things', c => c.json({ things: [] }))

Already have an instance? Pass it directly:

const limiter = await fixedWindow({ limit: 100, windowMs: 60_000 })
app.use('/api/*', rateLimit({ limiter }))

Options

OptionTypeDefaultDescription
limiterLimiter | (() => Limiter | Promise<Limiter>)requiredAny RateLock limiter. A factory is memoized and invoked once, on the first matched request.
keyGenerator(c) => string | Promise<string>remote addressIdentifier the request is counted against; falls back to a shared 'anonymous' bucket when no address is available.
headers'both' | 'rfc' | 'legacy' | false'both'Header families attached to responses.
limitnumber(none)Quota, used only to emit the *Limit headers.
denyStatusCodenumber429Status returned on exhaustion.
messagestring'Too Many Requests'JSON body of denial responses.

Response headers

FamilyHeaders
RFC 9331RateLimit-Limit, RateLimit-Remaining, RateLimit-Reset
LegacyX-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset
Denial onlyRetry-After

RateLimit-Reset is always in seconds. Token bucket results project floor(tokens) onto *Remaining and the refill time onto Reset.

Identifiers behind proxies

The default identifier is the runtime-provided remote address (Node socket, Bun requestIP, Deno remoteAddr). Behind a load balancer that address is your proxy's, so all clients would share one bucket. Configure your runtime's trust settings or provide a keyGenerator. Never read raw x-forwarded-for from untrusted clients: attackers can forge it per-request to bypass limits entirely.

How is this guide?

Last updated on

On this page