@animesh0764/pg-rate-limit
v0.1.0
Published
Postgres-powered rate limiting without Redis — works with Express, Fastify, Next.js, Hono
Maintainers
Readme
pg-rate-limit
Postgres-powered rate limiting without Redis.
Works with Express, Fastify, Next.js, and Hono. No Redis, no extra infrastructure — just the PostgreSQL you already have.
Install
npm install @animesh0764/pg-rate-limitpg is a peer dependency — install it if you haven't already:
npm install pgQuick start
import { Pool } from 'pg'
import { createRateLimiter } from '@animesh0764/pg-rate-limit'
const pool = new Pool({ connectionString: process.env.DATABASE_URL })
const limiter = createRateLimiter({
pool,
limit: 100,
window: '1m',
})
// Run once on startup to create the required tables
await limiter.setup()Framework examples
Express
// Global middleware — applies to every route
app.use(limiter.express())
// Custom identifier (e.g. authenticated user ID)
app.use(limiter.express({
identify: (req) => req.user?.id ?? req.ip ?? 'anonymous',
}))
// Skip rate limiting for certain requests
app.use(limiter.express({
skip: (req) => req.path === '/health',
}))
// Per-route with different limits
const strictLimiter = createRateLimiter({ pool, limit: 10, window: '1m' })
app.post('/auth/login', strictLimiter.express(), handler)Fastify
// As a preHandler hook
fastify.addHook('preHandler', limiter.fastify())
// Route-level
fastify.post('/api/action', {
preHandler: strictLimiter.fastify(),
handler: async (request, reply) => { ... }
})Next.js (App Router)
// app/api/data/route.ts
import { NextResponse } from 'next/server'
export const GET = limiter.nextjs()(async (req) => {
return NextResponse.json({ data: 'hello' })
})Hono
// Global
app.use('*', limiter.hono())
// Route group
app.use('/api/*', limiter.hono({
identify: (c) => c.req.header('authorization') ?? 'anonymous',
}))Configuration
const limiter = createRateLimiter({
pool, // pg.Pool instance (required)
limit: 100, // max requests per window (required)
window: '1m', // time window (required)
strategy: 'sliding', // 'sliding' (default) or 'fixed'
prefix: 'rl', // key prefix, default 'rl'
tableName: 'rate_limit', // table name prefix, default 'rate_limit'
})Window formats
| Format | Meaning |
|--------|---------|
| '500ms' | 500 milliseconds |
| '30s' | 30 seconds |
| '1m' | 1 minute |
| '1h' | 1 hour |
| '7d' | 7 days |
| 60000 | 60 seconds (raw ms) |
Strategies
Sliding window (default)
Counts requests in the last N seconds. Accurate but uses more database rows (one per request, deleted after the window expires).
Best for: API rate limits, login throttling, anything where accuracy matters.
Fixed window
Divides time into fixed buckets (e.g. minute :00–:59). Faster — one row per (identifier, window) pair. Has a known burst edge case: up to 2× the limit can pass when two bursts straddle a window boundary.
Best for: High-throughput scenarios where approximate limiting is acceptable.
Response headers
Every response includes standard rate limit headers:
| Header | Value |
|--------|-------|
| X-RateLimit-Limit | Configured limit |
| X-RateLimit-Remaining | Requests left in this window |
| X-RateLimit-Reset | Epoch second when the window resets |
| Retry-After | Seconds until retry (only on 429 responses) |
Programmatic check
const result = await limiter.check('user:abc123')
// { allowed: true, total: 100, remaining: 67, resetAt: Date }
if (!result.allowed) {
// handle rate limit in your own way
}Database setup
Tables created by setup()
rate_limit_fixed — used by the fixed window strategy:
identifier TEXT
window_start TIMESTAMPTZ -- start of the current window
window_end TIMESTAMPTZ -- end of the current window
count INTEGER -- requests in this windowrate_limit_sliding — used by the sliding window strategy:
identifier TEXT
requested_at TIMESTAMPTZ -- when the request arrivedCleanup
Old records accumulate over time. Call cleanup() periodically:
// Run once per hour (e.g. with a cron job)
await limiter.cleanup()Or schedule it with setInterval:
setInterval(() => limiter.cleanup(), 60 * 60 * 1000)Why not Redis?
Redis is excellent for rate limiting but adds operational overhead:
- One more service to deploy, monitor, and back up
- More infrastructure cost
- Connection management, eviction policies, persistence configuration
If you already have PostgreSQL and don't need sub-millisecond performance, pg-rate-limit gives you production-ready rate limiting with zero additional infrastructure.
License
MIT
