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
Node requires pg as a peer dependency. Bun uses native SQL — no extra install needed.
npm install pg
# or: pnpm add pg
# or: yarn add pg # No extra install needed
# Bun native SQL is built in. pg is only needed for the deprecated { pool } option. 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'
// Connection string — no extra dependencies
const limiter = hitlimit({
limit: 100,
window: '1m',
store: postgresStore({ url: 'postgres://localhost:5432/mydb' })
}) Options
| Option | Type | Default | Description |
|---|---|---|---|
url | string | - | Bun only. Connection string. Store creates and owns a Bun SQL client. |
client | SQL | - | Bun only. Caller-owned Bun SQL instance. Store does not close it. |
pool | Pool | - | A pg.Pool instance. Required for Node. Deprecated in Bun — use url or client. |
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 }) Pass a connection URL — Bun creates and owns the native SQL client:
postgresStore({ url: 'postgres://user:password@hostname:5432/mydb' }) Or pass a caller-owned Bun SQL instance (store will not close it):
import { SQL } from 'bun'
const sql = new SQL('postgres://user:password@hostname:5432/mydb')
postgresStore({ client: sql }) Full Configuration
postgresStore({
pool, // Node: pg.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: Node requires
pg(node-postgres). Bun uses native SQL — no extra dependencies.
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) |