Skip to content

API Reference

Creates a new rate limiter instance.

import { Pool } from "pg";
import { Ratelimit } from "pg-ratelimit";
const pool = new Pool({ connectionString: process.env.DATABASE_URL });
const ratelimit = new Ratelimit({
pool,
limiter: Ratelimit.slidingWindow(10, "1m"),
prefix: "api",
durable: false,
debug: false,
});

Config:

PropertyTypeDescription
poolpg.PoolA pg connection pool
limiterAlgorithmAlgorithm config from a static helper method
prefixstringRequired - namespaces keys to prevent collisions
durablebooleanOptional. Default: false - true uses logged table
synchronousCommitbooleanOnly when durable: true. Default: false - see RatelimitConfig
debugbooleanOptional. Default: false - logs queries to console.debug
clockClockOptional. Custom clock for testing. Defaults to () => new Date()
cleanupProbabilitynumberOptional. Default: 0.1 - probability (0–1) that a limit() call runs expired-row cleanup. See Cleanup strategy
inMemoryBlockbooleanOptional. Default: false - cache blocked keys in-process to skip DB round trips. See In-memory blocking
maxBlockedKeysnumberOnly when inMemoryBlock: true. Default: 10000 - OOM safety cap for cached entries

Auto-creates tables on first use via CREATE TABLE IF NOT EXISTS. Disable with PG_RATELIMIT_DISABLE_AUTO_MIGRATE=true.


Check and consume rate limit tokens. Also performs inline cleanup of expired rows for its prefix.

const result = await ratelimit.limit("user:123");
if (!result.success) {
return new Response("Too Many Requests", {
status: 429,
headers: { "Retry-After": String(Math.ceil((result.reset - Date.now()) / 1000)) },
});
}

Parameters:

ParameterTypeDefaultDescription
keystring-Identifier for the rate limit subject
opts.ratenumber1Tokens to consume. Negative values refund tokens

Returns: Promise<LimitResult>

Since rate controls how many tokens each call consumes, you can use it for weighted costs, per-tier differentiation, or refunds.

// Expensive operation costs 5 tokens
await ratelimit.limit("user:123", { rate: 5 });
// Refund after a failed upload
await ratelimit.limit("user:123", { rate: -1 });

Negative rates intentionally do not clamp to the max limit - tokens can exceed the maximum for admin/refund use cases.

Use rate to charge different costs per tier without creating separate instances:

await ratelimit.limit("user:123", {
rate: isPremium ? 1 : 5,
});

All methods (limit, blockUntilReady, getRemaining, resetUsedTokens) throw if the database is unreachable or returns an error. The original pg error propagates directly - nothing is wrapped or swallowed.

The library does not fail open or fail closed by default. You decide:

// Fail open - allow requests when DB is down
const result = await ratelimit.limit(key).catch(() => ({
success: true,
limit: 0,
remaining: 0,
reset: Date.now(),
}));
// Fail closed - deny requests when DB is down
const result = await ratelimit.limit(key).catch(() => ({
success: false,
limit: 0,
remaining: 0,
reset: Date.now(),
}));

ratelimit.blockUntilReady(key, timeout, opts?)

Section titled “ratelimit.blockUntilReady(key, timeout, opts?)”

Polls limit() until success or timeout. Uses reset from failed attempts to calculate sleep duration.

const result = await ratelimit.blockUntilReady("user:123", "30s");
// Block until 5 tokens are available
const result2 = await ratelimit.blockUntilReady("user:123", "30s", { rate: 5 });

Parameters:

ParameterTypeDescription
keystringIdentifier for the rate limit subject
timeoutDuration | numberMaximum time to wait
opts.ratenumberTokens to consume per attempt (default 1)

Returns: Promise<LimitResult>

Returns failure immediately (without sleeping) if the time until reset exceeds the remaining timeout.


Check remaining quota without consuming tokens.

const { remaining, reset } = await ratelimit.getRemaining("user:123");

Returns: Promise<{ remaining: number; reset: number }>


Full reset of a key’s quota. Useful for admin actions.

await ratelimit.resetUsedTokens("user:123");

Returns: Promise<void>


Exported constant containing the raw SQL for table creation. Use this if you disable auto-migration and manage schemas yourself.

import { TABLE_SQL } from "pg-ratelimit";
// Use in your migration tool of choice
await pool.query(TABLE_SQL);

Ratelimit.fixedWindow(10, "1m"); // 10 requests per minute
Ratelimit.fixedWindow(100, "1h"); // 100 requests per hour
ParameterTypeDescription
tokensnumberMax requests per window
windowDuration | numberWindow duration
Ratelimit.slidingWindow(50, "30s"); // 50 requests per 30 seconds
ParameterTypeDescription
tokensnumberMax requests per window
windowDuration | numberWindow duration

Ratelimit.tokenBucket(refillRate, interval, maxTokens)

Section titled “Ratelimit.tokenBucket(refillRate, interval, maxTokens)”
Ratelimit.tokenBucket(5, "10s", 20); // Refill 5 every 10s, max 20
ParameterTypeDescription
refillRatenumberTokens added per interval
intervalDuration | numberRefill interval
maxTokensnumberMaximum bucket capacity

interface LimitResult {
success: boolean; // Whether the request is allowed
limit: number; // Max tokens allowed
remaining: number; // Tokens remaining after this request
reset: number; // Unix timestamp (ms) when the current window/bucket resets
}

See new Ratelimit(config) above for all fields and defaults, and Database Design - synchronousCommit for details on how that option affects write performance and durability.

type TimeUnit = "s" | "m" | "h" | "d";
type Duration = `${number} ${TimeUnit}` | `${number}${TimeUnit}`;
// '30s', '30 s', '1h', '5m', '1d'
// Also accepts raw milliseconds as number where Duration | number is used
type Algorithm =
| { type: "fixedWindow"; tokens: number; window: Duration | number }
| { type: "slidingWindow"; tokens: number; window: Duration | number }
| { type: "tokenBucket"; refillRate: number; interval: Duration | number; maxTokens: number };
class Ratelimit {
constructor(config: RatelimitConfig);
limit(key: string, opts?: { rate?: number }): Promise<LimitResult>;
blockUntilReady(
key: string,
timeout: Duration | number,
opts?: { rate?: number },
): Promise<LimitResult>;
getRemaining(key: string): Promise<{ remaining: number; reset: number }>;
resetUsedTokens(key: string): Promise<void>;
static fixedWindow(tokens: number, window: Duration | number): Algorithm;
static slidingWindow(tokens: number, window: Duration | number): Algorithm;
static tokenBucket(refillRate: number, interval: Duration | number, maxTokens: number): Algorithm;
}
type Clock = () => Date;

Used for injectable time in tests. See the Testing Guide.


Under load, the vast majority of rate-limited requests are 429s that still make a full PostgreSQL round trip. inMemoryBlock caches blocked keys in a Map so that repeated requests from already-blocked keys return instantly without touching the database.

const ratelimit = new Ratelimit({
pool,
limiter: Ratelimit.slidingWindow(100, "1m"),
prefix: "api",
inMemoryBlock: true,
});
  1. When limit() returns success: false, the key and its reset timestamp are cached in memory.
  2. Subsequent limit() calls for the same key return a synthetic { success: false, remaining: 0 } result immediately - no DB query.
  3. Once the cached reset time passes, the entry is evicted and the next call falls through to the database normally.

Negative-rate calls (refunds) always bypass the cache and hit the database, since they may unblock a key. resetUsedTokens() also clears the cached entry for the key.


VariableDefaultDescription
PG_RATELIMIT_DISABLE_AUTO_MIGRATEfalseSet to true to skip automatic table creation

Code-level options always take precedence over environment variables.