Postgres Store
The Postgres store enables distributed rate limiting using your existing PostgreSQL database. Ideal for teams that already run Postgres and want to add rate limiting without introducing a new dependency like Redis.
Installation
The Postgres store requires pg as a peer dependency:
npm install pg
# or: pnpm add pg
# or: yarn add pg bun add pg Usage
import { hitlimit } from '@joint-ops/hitlimit'
import { postgresStore } from '@joint-ops/hitlimit/stores/postgres'
import { Pool } from 'pg'
const pool = new Pool({ connectionString: 'postgres://localhost:5432/mydb' })
app.use(hitlimit({
limit: 100,
window: '1m',
store: postgresStore({ pool })
})) import { hitlimit } from '@joint-ops/hitlimit-bun'
import { postgresStore } from '@joint-ops/hitlimit-bun/stores/postgres'
import { Pool } from 'pg'
const pool = new Pool({ connectionString: 'postgres://localhost:5432/mydb' })
const limiter = hitlimit({
limit: 100,
window: '1m',
store: postgresStore({ pool })
}) Options
| Option | Type | Default | Description |
|---|---|---|---|
pool | Pool | - | A pg.Pool instance (required) |
tablePrefix | string | 'hitlimit' | Prefix for rate limit table names |
cleanupInterval | number | 60000 | Interval (ms) to clean expired entries |
skipTableCreation | boolean | false | Skip automatic table creation on startup |
Connection Options
Create a pool with a connection string:
import { Pool } from 'pg'
const pool = new Pool({
connectionString: 'postgres://user:password@hostname:5432/mydb'
}) Or use individual connection parameters:
const pool = new Pool({
host: 'localhost',
port: 5432,
database: 'mydb',
user: 'postgres',
password: 'secret',
max: 20 // max pool size
})
postgresStore({ pool }) Full Configuration
postgresStore({
pool,
tablePrefix: 'api_limits',
cleanupInterval: 300000, // Clean every 5 minutes
skipTableCreation: false
}) Named Prepared Statements
The Postgres store uses named prepared statements for all hot-path queries. PostgreSQL caches the query plan for named statements, avoiding repeated parsing and planning overhead on every rate limit check.
This provides two key benefits:
- Lower latency — query plans are cached and reused across calls
- True atomicity —
ON CONFLICT ... DO UPDATEupserts prevent race conditions
For endpoints with ban detection enabled, the store handles increment, expiry, violation tracking, and ban threshold checking through dedicated prepared statements.
Characteristics
- Speed: Network-bound (latency depends on Postgres server location and network)
- Persistence: Full PostgreSQL durability (WAL, replication)
- Scalability: Shared across all server instances
- Atomic: Upsert queries ensure race-condition-free increments
- Dependencies: Requires
pg(node-postgres)
When to Use
- Teams already running PostgreSQL in production
- Multi-server/container deployments without Redis
- When you want distributed rate limiting with SQL-level observability
- Environments where adding Redis is not feasible or desired
Database Schema
The store automatically creates the following tables (prefix defaults to hitlimit):
CREATE TABLE IF NOT EXISTS hitlimit_hits (
key TEXT PRIMARY KEY,
count INTEGER NOT NULL DEFAULT 1,
reset_at BIGINT NOT NULL
)
CREATE TABLE IF NOT EXISTS hitlimit_bans (
key TEXT PRIMARY KEY,
expires_at BIGINT NOT NULL
)
CREATE TABLE IF NOT EXISTS hitlimit_violations (
key TEXT PRIMARY KEY,
count INTEGER NOT NULL DEFAULT 1,
reset_at BIGINT NOT NULL
) Postgres vs Redis
| Aspect | Postgres | Redis |
|---|---|---|
| Latency | Network-bound | Network-bound |
| Throughput | Network-bound | Network-bound (typically faster) |
| Extra infra | None (use existing DB) | Requires Redis server |
| Observability | SQL queries, pg_stat | Redis CLI, MONITOR |
| Durability | Full ACID | Configurable (RDB/AOF) |