A simple and efficient rate limiting utility using @upstash/redis and Lua scripting for atomicity. Designed for serverless and scalable applications.
- β‘ Lightning-fast using Upstash Redis (HTTP-based)
- β Atomic rate limiting using Lua scripts
- π§ Tracks request count per user/IP
- π« Blocks requests once the limit is reached
- π Automatically resets after the time window
βββ lib/
β βββ redis.ts # Redis client setup
βββ utils/
β βββ rateLimiter.ts # Rate limiter logic
βββ .env.local # Environment variables
βββ README.md
npm install @upstash/redis
# or
yarn add @upstash/redisCreate a .env.local file at the root of your project and add your Upstash Redis credentials:
UPSTASH_REDIS_REST_URL=https://your-upstash-url.upstash.io
UPSTASH_REDIS_REST_TOKEN=your-secret-tokenYou can find these in your Upstash Console.
import { Redis } from "@upstash/redis";
function getEnvOrThrow() {
const url = process.env.UPSTASH_REDIS_REST_URL;
const token = process.env.UPSTASH_REDIS_REST_TOKEN;
if (!url || !token) {
throw new Error(
"β Missing Upstash Redis credentials. Please provide UPSTASH_REDIS_REST_URL and UPSTASH_REDIS_REST_TOKEN in your environment variables."
);
}
return { url, token };
}
export const redis = new Redis(getEnvOrThrow());import { redis } from "@/lib/redis";
const WINDOW_IN_SECONDS = 60; // 1 minute
const MAX_REQUESTS = 30; // max requests per IP per minute
const KEY_PREFIX = "rate_limit:";
// Lua script to ensure atomic operations in Redis
const RATE_LIMIT_SCRIPT = `
local key = KEYS[1]
local window = tonumber(ARGV[1])
local current = redis.call('INCR', key)
if current == 1 then
redis.call('EXPIRE', key, window)
end
local ttl = redis.call('TTL', key)
return { current, ttl }
`;
export const rateLimiter = {
limit: async (identifier: string) => {
const key = `${KEY_PREFIX}${identifier}`;
try {
const [currentRequests, ttl] = (await redis.eval(
RATE_LIMIT_SCRIPT,
[key],
[WINDOW_IN_SECONDS.toString()]
)) as [number, number];
const remaining = Math.max(0, MAX_REQUESTS - currentRequests);
const response = {
success: true,
allowed: currentRequests <= MAX_REQUESTS,
limit: MAX_REQUESTS,
remaining,
reset: ttl,
retryAfter: ttl,
window: WINDOW_IN_SECONDS,
};
if (!response.allowed) {
response.success = false;
response.remaining = 0;
}
return response;
} catch (error) {
console.error("Rate limiter error:", error);
return {
success: true,
allowed: true,
limit: MAX_REQUESTS,
remaining: MAX_REQUESTS,
reset: WINDOW_IN_SECONDS,
retryAfter: 0,
window: WINDOW_IN_SECONDS,
};
}
},
};In an API route or middleware:
import { rateLimiter } from "@/utils/rateLimiter";
import type { NextApiRequest, NextApiResponse } from "next";
export default async function handler(
req: NextApiRequest,
res: NextApiResponse
) {
const ip =
req.headers["x-forwarded-for"] || req.socket.remoteAddress || "unknown";
const result = await rateLimiter.limit(ip.toString());
if (!result.allowed) {
return res.status(429).json({
message: "Too many requests. Please try again later.",
retryAfter: result.retryAfter,
});
}
// Proceed with your logic
res.status(200).json({ message: "OK" });
}In a middleware:
import { rateLimiter } from "@/utils/rateLimiter";
import { type NextRequest, NextResponse } from "next/server";
import { ipAddress } from "@vercel/functions";
export default async function middleware(req: NextRequest) {
const response = NextResponse.next();
const clientIp =
req.headers.get("x-forwarded-for") ||
ipAddress(req) ||
req.headers.get("x-real-ip") ||
"127.0.0.1";
response.headers.s;
if (pathname.startsWith("/api")) {
const limit = await rateLimiter.limit(clientIp);
if (!limit.allowed) {
response.headers.set("X-RateLimit-Limit", limit.limit.toString());
response.headers.set("X-RateLimit-Remaining", limit.remaining.toString());
response.headers.set("X-RateLimit-Reset", limit.reset.toString());
response.headers.set("Retry-After", limit.retryAfter.toString());
// return res.status(429).json({
// error: `Too many requests. Retry after ${limit.retryAfter} seconds`,
// });
throw new AppError(tooManyRequestsTranslate[lang].title, 429);
// NextResponse.redirect(new URL(CUSTOM_ERROR_PATH, req.url));
}
// Apply rate limit headers to all responses
response.headers.set("X-RateLimit-Limit", limit.limit.toString());
response.headers.set("X-RateLimit-Remaining", limit.remaining.toString());
response.headers.set("X-RateLimit-Reset", limit.reset.toString());
}
}Using a Lua script in Redis ensures atomic operations, meaning no two requests can modify the rate limit count at the same timeβensuring accuracy.
- Protect login routes
- Throttle email/password reset forms
- Rate limit API access per user/IP/device
- You can adjust the rate window and limit by changing
WINDOW_IN_SECONDSandMAX_REQUESTS. - You can use session tokens, API keys, or IPs as
identifier.
- Upstash Redis
- Inspired by concepts from Cloudflare Workers and Vercel Edge Functions.