Documentation Index
Fetch the complete documentation index at: https://mintlify.com/AmeyaBorkar/throttlekit/llms.txt
Use this file to discover all available pages before exploring further.
PostgresStore gives you exact, distributed rate limiting using your existing PostgreSQL database — no Redis, no extra infrastructure. Each apply call runs the limiter’s pure JavaScript transform inside a transaction serialized per key by a transaction-scoped advisory lock. This makes N concurrent checks from any number of processes land exactly N decrements, identical to what Redis’s atomic EVALSHA guarantees.
How it works
SELECT … FOR UPDATE — is used deliberately: FOR UPDATE cannot lock a row that does not yet exist, so two first-touch transactions on a new key could race. An advisory lock keyed by the hash of the key serializes them whether or not the row exists, and auto-releases at COMMIT or ROLLBACK so an error can never leak a held lock.
State is stored as JSON text, identical to what the Redis optimistic-concurrency path writes. A value round-trips as the exact IEEE-754 double, keeping decisions bit-identical across all backends.
Installation
PostgresStore is exported from the throttlekit/postgres subpath. You also need the pg (node-postgres) package as a peer dependency.
Quick example
Schema
WhenautoCreate: true (the default), the store runs these statements on first use — no manual migration required:
table option. Both schema.table and bare table identifiers are accepted.
Options
A
pg.Pool (or any compatible object with connect() and query() methods).
ThrottleKit never ends a pool it does not own — call pool.end() yourself
when your application shuts down.Unquoted table identifier for the limiter state table. Validated against
^[A-Za-z_][A-Za-z0-9_]*$ (optionally schema.table) since identifiers
cannot be parameterized. Change this if you want to share a schema with other
application tables.Storage key namespace. Keys are stored as
prefix:key. Use this to share
one table across multiple limiters without key collisions.Create the table and its expiry index on first use. Set
false when you
manage migrations externally (e.g. with Flyway, Liquibase, or a custom SQL
migration script) and want ThrottleKit to assume the table already exists.Interval in milliseconds for the background sweep that deletes rows whose
expires_at has passed. Set 0 to disable the sweep entirely and rely on
lazy expiry only — expired rows are already invisible to reads and do not
affect correctness; the sweep only reclaims storage.Time source for expiry decisions. Defaults to the system clock. Inject a
ManualClock to drive expiry deterministically in tests.When to use
PostgresStore is a good fit when:- Your stack already runs Postgres and you want to avoid adding Redis.
- You need durable counters that survive both application and database restarts.
- You are running on a managed Postgres service (RDS, Cloud SQL, Supabase, Neon) and want to keep the architecture simple.
- Your rate limits are billing-critical or abuse-critical and you need the persistence guarantees of a transactional database.
Performance notes
Each check is one transaction — aBEGIN, one advisory lock acquisition, one SELECT, one INSERT … ON CONFLICT, and a COMMIT. With a local Postgres that is typically 1–3 ms. For hot keys where that round-trip cost matters, wrap the PostgresStore as the L2 of twoTier({ mode: "leased" }): the in-process tier absorbs the hot path and the Postgres tier enforces the global cap with far fewer transactions.
PostgresStore is async-only. limiter.checkSync(key) will throw at
runtime — always use await limiter.check(key) with this backend.Failure behavior
When the Postgres pool cannot reach the database,apply rejects with a pg error. The limiter’s fail policy ("open" or "closed") determines whether a failed check admits or denies the request. Rate-limit state is preserved in the Postgres table across reconnects, connection pool recycling, and even a database restart — the counts resume exactly from where they left off.