On this page

Fastify Adapter

The Fastify adapter provides seamless integration with Fastify applications through a native plugin using the onRequest hook.

Installation

npm install @joint-ops/hitlimit fastify fastify-plugin
pnpm add @joint-ops/hitlimit fastify fastify-plugin
yarn add @joint-ops/hitlimit fastify fastify-plugin
bun add @joint-ops/hitlimit fastify fastify-plugin

Basic Usage

Register hitlimit as a Fastify plugin:

app.ts
import Fastify from 'fastify'
import { hitlimit } from '@joint-ops/hitlimit/fastify'

const app = Fastify()

// Apply to all routes
await app.register(hitlimit, {
  limit: 100,
  window: '1m'
})

app.get('/', () => 'Hello')

await app.listen({ port: 3000 })

Route-Specific Rate Limiting

Use Fastify's encapsulated plugin system to apply different limits per route prefix:

routes.ts
// Strict limit on /api routes
app.register(async (scope) => {
  await scope.register(hitlimit, {
    limit: 100,
    window: '1m'
  })
  scope.get('/users', handler)
}, { prefix: '/api' })

// Tight limit on auth routes
app.register(async (scope) => {
  await scope.register(hitlimit, {
    limit: 5,
    window: '15m'
  })
  scope.post('/login', handler)
}, { prefix: '/auth' })

Custom Key Extraction

Rate limit by user ID, API key, or custom identifiers:

await app.register(hitlimit, {
  limit: 100,
  window: '1m',
  key: (request) => {
    // Rate limit by API key or fall back to IP
    return request.headers['x-api-key'] || request.ip
  }
})

Skipping Requests

Bypass rate limiting for certain requests:

await app.register(hitlimit, {
  limit: 100,
  window: '1m',
  skip: (request) => {
    // Skip health checks
    return request.url === '/health'
  }
})

Custom Error Response

Customize the response when rate limit is exceeded:

await app.register(hitlimit, {
  limit: 100,
  window: '1m',
  response: (info) => ({
    error: 'Rate limit exceeded',
    retryIn: info.resetIn
  })
})

Using with Stores

Use Redis or SQLite for distributed rate limiting:

import { hitlimit } from '@joint-ops/hitlimit/fastify'
import { redisStore } from '@joint-ops/hitlimit/stores/redis'

await app.register(hitlimit, {
  limit: 100,
  window: '1m',
  store: redisStore({
    url: 'redis://localhost:6379'
  })
})

Migrating from @fastify/rate-limit

Switching from @fastify/rate-limit to hitlimit is straightforward. Both use Fastify's plugin system.

Before (@fastify/rate-limit)
import rateLimit from '@fastify/rate-limit'

await app.register(rateLimit, {
  max: 100,
  timeWindow: '1 minute'
})
After (hitlimit)
import { hitlimit } from '@joint-ops/hitlimit/fastify'

await app.register(hitlimit, {
  limit: 100,
  window: '1m'
})
@fastify/rate-limit hitlimit
max: 100 limit: 100
timeWindow: '1 minute' window: '1m'
keyGenerator: (req) => ... key: (req) => ...
errorResponseBuilder: (req, ctx) => ... response: (info) => ...
No built-in tiered limits tiers: { free: ..., pro: ... }
No built-in SQLite store store: sqliteStore()

See the full comparison for more details.

Advanced: Ban + Group Features

hitlimit supports auto-banning repeat offenders and shared rate limits across groups.

Auto-Ban After Threshold Violations

Automatically ban IPs that violate rate limits repeatedly:

await app.register(hitlimit, {
  limit: 100,
  window: '1m',
  ban: {
    threshold: 5,      // Ban after 5 violations
    duration: '15m'    // Ban lasts 15 minutes
  }
})

When a client exceeds the rate limit 5 times within the tracking window, they'll be banned for 15 minutes. During the ban, all requests return 429 immediately.

Shared Rate Limits via GroupId

Share rate limits across multiple clients using a group identifier:

await app.register(hitlimit, {
  limit: 1000,
  window: '1h',
  group: (request) => {
    // Share limit across user's team
    return request.user?.teamId || request.user?.tier || 'free'
  }
})

All requests with the same group ID share the same rate limit counter. Perfect for team-based limits in SaaS applications.

Next Steps