@joint-ops/hitlimit-bun
v1.4.0
Published
Ultra-fast Bun-native rate limiting - Memory-first with 6M+ ops/sec for Bun.serve, Elysia & Hono
Keywords
Readme
@joint-ops/hitlimit-bun
Rate limiting built for Bun. Not ported — built.
7.73M ops/sec on memory. 5.57M at 10K IPs. Native bun:sqlite. Atomic Redis Lua. Postgres. Zero dependencies.
bun add @joint-ops/hitlimit-bunBun.serve({
fetch: hitlimit({}, (req) => new Response('Hello!'))
})One line. Done. Works with Bun.serve, Elysia, and Hono out of the box.
Docs · GitHub · Benchmarks
30 Seconds to Production
Bun.serve
import { hitlimit } from '@joint-ops/hitlimit-bun'
Bun.serve({
fetch: hitlimit({ limit: 100, window: '1m' }, (req) => {
return new Response('Hello!')
})
})Elysia
import { Elysia } from 'elysia'
import { hitlimit } from '@joint-ops/hitlimit-bun/elysia'
new Elysia()
.use(hitlimit({ limit: 100, window: '1m' }))
.get('/', () => 'Hello!')
.listen(3000)Hono
import { Hono } from 'hono'
import { hitlimit } from '@joint-ops/hitlimit-bun/hono'
const app = new Hono()
app.use(hitlimit({ limit: 100, window: '1m' }))
app.get('/', (c) => c.text('Hello!'))
Bun.serve({ port: 3000, fetch: app.fetch })What You Get
Tiered limits — Free, Pro, Enterprise:
hitlimit({
tiers: { free: { limit: 100, window: '1h' }, pro: { limit: 5000, window: '1h' } },
tier: (req) => req.headers.get('x-tier') || 'free'
}, handler)Auto-ban — Repeat offenders get blocked:
hitlimit({ limit: 10, window: '1m', ban: { threshold: 5, duration: '1h' } }, handler)Custom keys — Rate limit by anything:
hitlimit({ key: (req) => req.headers.get('x-api-key') || 'anon' }, handler)Route-specific limits (Elysia):
new Elysia()
.use(hitlimit({ limit: 100, window: '1m', name: 'global' }))
.group('/auth', app => app.use(hitlimit({ limit: 5, window: '15m', name: 'auth' })))
.listen(3000)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) (bun:sqlite) │ Valkey │ MongoDB │
│ │ │ Dragonfly MySQL │
└──────────────────────┘ └──────────────────────────┘
No dependencies at all Your existing infra, zero lock-in| Store | Ops/sec | Latency | When to use | |-------|---------|---------|-------------| | Memory | 5,574,103 | 179ns | Single server, maximum speed | | bun:sqlite | 372,247 | 2.7μs | Single server, need persistence | | MongoDB | 2,132 | 469μ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-bun'
import { ______Store } from '@joint-ops/hitlimit-bun/stores/______'
Bun.serve({ fetch: hitlimit({ store: ______Store({ /* config */ }) }, handler) })Bun.serve({ fetch: hitlimit({}, handler) }) // that's itimport { sqliteStore } from '@joint-ops/hitlimit-bun'
Bun.serve({ fetch: hitlimit({ store: sqliteStore({ path: './ratelimit.db' }) }, handler) })No peer dependency — bun:sqlite is built into Bun.
import { redisStore } from '@joint-ops/hitlimit-bun/stores/redis'
Bun.serve({ fetch: hitlimit({ store: redisStore({ url: 'redis://localhost:6379' }) }, handler) })Peer dep: ioredis
import { valkeyStore } from '@joint-ops/hitlimit-bun/stores/valkey'
Bun.serve({ fetch: hitlimit({ store: valkeyStore({ url: 'redis://localhost:6379' }) }, handler) })Peer dep: ioredis
import { dragonflyStore } from '@joint-ops/hitlimit-bun/stores/dragonfly'
Bun.serve({ fetch: hitlimit({ store: dragonflyStore({ url: 'redis://localhost:6379' }) }, handler) })Peer dep: ioredis
import { postgresStore } from '@joint-ops/hitlimit-bun/stores/postgres'
Bun.serve({ fetch: hitlimit({ store: postgresStore({ url: 'postgres://localhost:5432/mydb' }) }, handler) })Peer dep: pg
import { mongoStore } from '@joint-ops/hitlimit-bun/stores/mongodb'
import { MongoClient } from 'mongodb'
const client = new MongoClient('mongodb://localhost:27017')
const db = client.db('myapp')
Bun.serve({ fetch: hitlimit({ store: mongoStore({ db }) }, handler) })Peer dep: mongodb
import { mysqlStore } from '@joint-ops/hitlimit-bun/stores/mysql'
import mysql from 'mysql2/promise'
const pool = mysql.createPool('mysql://root@localhost:3306/mydb')
Bun.serve({ fetch: hitlimit({ store: mysqlStore({ pool }) }, handler) })Peer dep: mysql2
Performance
Bun vs Node.js — Memory Store, 10K unique IPs
| Runtime | Ops/sec | | |---------|---------|---| | Bun | 5,574,103 | ████████████████████ | | Node.js | 4,082,874 | ███████████████ |
Bun leads at 10K IPs (5.57M vs 4.08M) and single-IP (7.73M vs 5.96M). Same library, same algorithm, memory store. For Redis, Postgres, and cross-store breakdowns, see the full benchmark results. Controlled-environment microbenchmarks with transparent methodology. Run them yourself.
Why bun:sqlite doesn't need bindings
Node.js: JS → N-API → C++ binding → SQLite
Bun: JS → Native call → SQLite (no overhead)No N-API. No C++ bindings. No FFI. Bun calls SQLite directly.
Related
- @joint-ops/hitlimit — Node.js variant for Express, Fastify, Hono, NestJS
License
MIT
