@joint-ops/hitlimit
v1.4.0
Published
Fast rate limiting middleware for Express, Fastify, NestJS & Node.js - API throttling, brute force protection, request limiting
Keywords
Readme
@joint-ops/hitlimit
Rate limiting that doesn't slow you down.
4.08M ops/sec at 10K unique IPs. Zero dependencies. One line to protect any Node.js API.
npm install @joint-ops/hitlimitapp.use(hitlimit()) // That's it. 100 req/min per IP.Works with Express, Fastify, Hono, NestJS, and native HTTP — no config, no adapters to install, no boilerplate.
Docs · GitHub · Benchmarks
30 Seconds to Production
Express
import express from 'express'
import { hitlimit } from '@joint-ops/hitlimit'
const app = express()
app.use(hitlimit({ limit: 100, window: '15m' }))Fastify
import { hitlimit } from '@joint-ops/hitlimit/fastify'
await app.register(hitlimit, { limit: 100, window: '1m' })Hono
import { hitlimit } from '@joint-ops/hitlimit/hono'
app.use(hitlimit({ limit: 100, window: '1m' }))NestJS
import { HitLimitModule, HitLimitGuard } from '@joint-ops/hitlimit/nest'
@Module({
imports: [HitLimitModule.register({ limit: 100, window: '1m' })],
providers: [{ provide: APP_GUARD, useClass: HitLimitGuard }]
})
export class AppModule {}Native HTTP
import { createHitLimit } from '@joint-ops/hitlimit/node'
const limiter = createHitLimit({ limit: 100, window: '1m' })
const server = http.createServer(async (req, res) => {
const result = await limiter.check(req)
if (!result.allowed) return res.writeHead(429).end()
res.end('OK')
})What You Get
Tiered limits — Free, Pro, Enterprise in 4 lines:
hitlimit({
tiers: { free: { limit: 100, window: '1h' }, pro: { limit: 5000, window: '1h' } },
tier: (req) => req.user?.plan || 'free'
})Auto-ban — Ban repeat offenders automatically:
hitlimit({ limit: 100, window: '1m', ban: { threshold: 5, duration: '15m' } })Custom keys — Rate limit by IP, user, API key, or anything:
hitlimit({ key: (req) => req.headers['x-api-key'] || req.ip })Shared limits — Team-wide quotas with group:
hitlimit({ limit: 10000, window: '1h', group: (req) => req.user.teamId })Skip rules — Whitelist health checks, admins, internal routes:
hitlimit({ skip: (req) => req.path === '/health' || req.user?.role === 'admin' })Pick Your Store
Every store is built in. Swap one line — your rate limiting code stays the same.
Single Server Multi-Server
┌──────────────────────┐ ┌──────────────────────────┐
│ Memory │ SQLite │ │ Redis │ Postgres │
│ (default) │ │ Valkey │ MongoDB │
│ │ │ Dragonfly MySQL │
└──────────────────────┘ └──────────────────────────┘
No persistence Your existing infrastructure, zero lock-in| Store | Ops/sec | Latency | When to use | |-------|---------|---------|-------------| | Memory | 4,082,874 | 245ns | Single server, no persistence needed | | SQLite | 404,135 | 2.5μs | Single server, need persistence | | MongoDB | 2,161 | 462.8μs | Multi-server / NoSQL infrastructure |
Redis, Valkey, DragonflyDB, Postgres, and MySQL are network-bound (~200–3,500 ops/sec). Benchmarks at hitlimit.jointops.dev/docs/benchmarks.
The pattern is always the same
import { hitlimit } from '@joint-ops/hitlimit'
import { ______Store } from '@joint-ops/hitlimit/stores/______'
app.use(hitlimit({ store: ______Store({ /* connection config */ }) }))app.use(hitlimit()) // that's itimport { sqliteStore } from '@joint-ops/hitlimit/stores/sqlite'
app.use(hitlimit({ store: sqliteStore({ path: './ratelimit.db' }) }))Peer dep: better-sqlite3
import { redisStore } from '@joint-ops/hitlimit/stores/redis'
app.use(hitlimit({ store: redisStore({ url: 'redis://localhost:6379' }) }))Peer dep: ioredis
import { valkeyStore } from '@joint-ops/hitlimit/stores/valkey'
app.use(hitlimit({ store: valkeyStore({ url: 'redis://localhost:6379' }) }))Peer dep: ioredis
import { dragonflyStore } from '@joint-ops/hitlimit/stores/dragonfly'
app.use(hitlimit({ store: dragonflyStore({ url: 'redis://localhost:6379' }) }))Peer dep: ioredis
import { postgresStore } from '@joint-ops/hitlimit/stores/postgres'
app.use(hitlimit({ store: postgresStore({ url: 'postgres://localhost:5432/mydb' }) }))Peer dep: pg
import { mongoStore } from '@joint-ops/hitlimit/stores/mongodb'
import { MongoClient } from 'mongodb'
const client = new MongoClient('mongodb://localhost:27017')
const db = client.db('myapp')
app.use(hitlimit({ store: mongoStore({ db }) }))Peer dep: mongodb
import { mysqlStore } from '@joint-ops/hitlimit/stores/mysql'
import mysql from 'mysql2/promise'
const pool = mysql.createPool('mysql://root@localhost:3306/mydb')
app.use(hitlimit({ store: mysqlStore({ pool }) }))Peer dep: mysql2
Performance
vs Competitors (Memory Store, 10K unique IPs)
| Library | Ops/sec | | |---------|---------|---| | hitlimit | 4,082,874 | ████████████████████ | | rate-limiter-flexible | 1,261,659 | ██████ | | express-rate-limit | 824,030 | ████ |
This is the memory store comparison — the default for all three libraries. For Redis, Postgres, and cross-store breakdowns, see the full benchmark results. Controlled-environment microbenchmarks with transparent methodology. We report scenarios where competitors beat us. Run them yourself.
Migrating?
// express-rate-limit → hitlimit
- import rateLimit from 'express-rate-limit'
- app.use(rateLimit({ windowMs: 60000, max: 100 }))
+ import { hitlimit } from '@joint-ops/hitlimit'
+ app.use(hitlimit({ window: '1m', limit: 100 }))Full migration guide in the docs.
Related
- @joint-ops/hitlimit-bun — Bun-native variant with native bun:sqlite
License
MIT
