Table of Contents
Drizzle ratelimit
Basic ratelimiting example with drizzle
WIP. Use at your own risk.
Define database schema
import { sqliteTable, integer, text } from 'drizzle-orm/sqlite-core';
export const ratelimit = sqliteTable('ratelimit', {
key: text('key').primaryKey(),
points: integer('points').notNull(),
expire: integer('expire', { mode: "timestamp" }),
maxPoints: integer('maxPoints').notNull(),
durationMs: integer('durationMs').notNull(),
})
Ratelimit function
import { db } from "./db"; // drizzle instance
import { ratelimit } from "./db/schema";
import { eq } from "drizzle-orm";
interface RateLimitOptions {
/** Maximum number of points allowed within the duration window */
points?: number;
/** Time window duration in seconds */
duration?: number; // in seconds
/** Number of points to consume per request */
consumePoints?: number;
}
const defaultRateLimiter = {
points: 1,
duration: 1,
}
export async function rateLimit(
userId: string,
options: RateLimitOptions = {}
): Promise<{
rateLimited: boolean,
remainingPoints: number,
msBeforeNext: number,
consumedPoints: number,
isFirstInDuration: boolean
}> {
const {
points = defaultRateLimiter.points,
duration = defaultRateLimiter.duration,
consumePoints = 1
} = options;
const now = new Date();
const nowMs = now.getTime();
const durationMs = duration * 1000;
const configKey = `${userId}:${points}:${duration}`;
const existingRecord = await db
.select()
.from(ratelimit)
.where(eq(ratelimit.key, configKey))
.get();
let isFirstInDuration = false;
if (!existingRecord) {
isFirstInDuration = true;
const expireTime = new Date(nowMs + durationMs);
if (consumePoints > points) {
return {
rateLimited: true,
remainingPoints: points,
msBeforeNext: durationMs,
consumedPoints: 0,
isFirstInDuration
};
}
await db
.insert(ratelimit)
.values({
key: configKey,
points: consumePoints,
expire: expireTime,
maxPoints: points,
durationMs: durationMs
});
return {
rateLimited: false,
remainingPoints: points - consumePoints,
msBeforeNext: durationMs,
consumedPoints: consumePoints,
isFirstInDuration
};
}
const expireMs = existingRecord.expire?.getTime() || 0;
const storedMaxPoints = existingRecord.maxPoints;
const storedDurationMs = existingRecord.durationMs;
const configChanged = storedMaxPoints !== points || storedDurationMs !== durationMs;
if (nowMs >= expireMs || configChanged) {
isFirstInDuration = true;
const newExpireTime = new Date(nowMs + durationMs);
if (consumePoints > points) {
await db
.update(ratelimit)
.set({
points: 0,
expire: newExpireTime,
maxPoints: points,
durationMs: durationMs
})
.where(eq(ratelimit.key, configKey));
return {
rateLimited: true,
remainingPoints: points,
msBeforeNext: durationMs,
consumedPoints: 0,
isFirstInDuration
};
}
await db
.update(ratelimit)
.set({
points: consumePoints,
expire: newExpireTime,
maxPoints: points,
durationMs: durationMs
})
.where(eq(ratelimit.key, configKey));
return {
rateLimited: false,
remainingPoints: points - consumePoints,
msBeforeNext: durationMs,
consumedPoints: consumePoints,
isFirstInDuration
};
}
const currentPoints = existingRecord.points;
const newPoints = currentPoints + consumePoints;
if (newPoints > points) {
const msBeforeNext = expireMs - nowMs;
return {
rateLimited: true,
remainingPoints: Math.max(0, points - currentPoints),
msBeforeNext: Math.max(0, msBeforeNext),
consumedPoints: 0, // No points consumed when rate limited
isFirstInDuration: false
};
}
await db
.update(ratelimit)
.set({
points: newPoints
})
.where(eq(ratelimit.key, configKey));
const msBeforeNext = expireMs - nowMs;
return {
rateLimited: false,
remainingPoints: points - newPoints,
msBeforeNext: Math.max(0, msBeforeNext),
consumedPoints: consumePoints,
isFirstInDuration: false
};
}
Usage
Make sure to use an identifier that is unique to the user. For example, a user id, session id or api key. If you use the ratelimit function for multiple different actions, make sure to use a unique identifier for each action. for example userId:actionName
.
const identifier = "abc" // can be a userid, sessionid, api key etc
const ratelimit = await rateLimit(identifier, {
consumePoints: 1, // Number of points to consume per request
duration: 1, // Time window duration in seconds
points: 3 // Maximum number of points allowed within the duration window
})
// Returns the following object
const ratelimit = {
rateLimited: false,
remainingPoints: 2,
msBeforeNext: 1000,
consumedPoints: 1,
isFirstInDuration: false
}