Rate Limiter
Framework-agnostic rate limiting with pluggable storage backends and RFC-compliant headers.
The Rate Limiter block protects your API from abuse by capping how many requests a client can make within a configurable time window.
Instead of coupling rate limiting to a single framework or opaque npm dependency, it gives you a shared core engine with lightweight adapters for Express, Fastify, and Hono — and pluggable storage via Memory or Redis.
Features
- Framework-agnostic core engine shared across Express, Fastify, and Hono
- Pluggable storage: in-memory
Mapwith chronological purge or Redis via atomic Lua script - Configurable time window, request limit, and HTTP status code
- Custom key generation — default extracts client IP, or use any identifier
- RFC-compliant
RateLimit-*standard headers - Legacy
X-RateLimit-*headers for backward compatibility Retry-Afterheader on blocked requests- Fail-open on store errors — a storage outage never takes down your API
File Structure
rate-limiter
├── adapters
│ ├── express.ts
│ ├── fastify.ts
│ └── hono.ts
├── core
│ └── core.ts
├── utils
│ └── ip.ts
└── variants
├── memory-store.ts
└── redis-store.ts- adapters/ — Framework-specific middleware that extracts the client key, invokes the core engine, and applies response headers.
- core/ — The
evaluateRateLimitengine and shared TypeScript interfaces (RateLimitConfig,RateLimitStore,RateLimitRecord,RateLimitResult). - utils/ —
getClientIputility for extracting the client IP from proxy headers or socket address. - variants/ — Storage implementations:
MemoryStore(in-memory map with periodic incremental purge) andRedisStore(atomic Lua script via ioredis).
Installation
pnpm dlx blockend-cli add rate-limiterDetect Project
Blockend detects your project configuration and determines the correct output location.
Select Adapter
? Which adapter would you like?
❯ Express
Fastify
HonoSelect Store
? Which store would you like?
❯ Memory
RedisInstall Dependencies
Required packages (e.g. ioredis for Redis) are installed automatically.
Generate Files
The selected adapter, store, and base files are generated into your project.
Copy the files below into your project's blocks directory.
Peer Dependencies
| Package | Required for |
|---|---|
express | Express adapter |
fastify | Fastify adapter |
hono | Hono adapter |
ioredis | Redis store |
blocks/rate-limiter/core/core.ts
Core rate limiting engine and shared type definitions: evaluateRateLimit, RateLimitConfig, RateLimitStore, RateLimitRecord, and RateLimitResult.
// --- Core Interfaces ---
export interface RateLimitRecord {
hits: number;
resetTime: number; // Unix timestamp in ms
}
export interface RateLimitStore {
increment(key: string, windowMs: number): Promise<RateLimitRecord> | RateLimitRecord;
}
export interface RateLimitConfig {
windowMs: number;
max: number;
statusCode: number;
message: string | Record<string, unknown>;
standardHeaders: boolean;
legacyHeaders: boolean; // <-- Added flag for backwards compatibility
}
export interface RateLimitResult {
isBlocked: boolean;
statusCode: number;
message: string | Record<string, unknown>;
headers: Record<string, string>;
}
// --- The Core Engine ---
export async function evaluateRateLimit(
key: string,
store: RateLimitStore,
config: RateLimitConfig
): Promise<RateLimitResult> {
const { hits, resetTime } = await store.increment(key, config.windowMs);
const remaining = Math.max(0, config.max - hits);
const now = Date.now();
const retryAfterSeconds = Math.ceil(Math.max(0, resetTime - now) / 1000);
const headers: Record<string, string> = {};
const isBlocked = hits > config.max;
// 1. Modern Standard Headers (RFC 9421 Draft Compliant)
if (config.standardHeaders) {
headers["RateLimit-Limit"] = `${config.max}, window=${Math.ceil(config.windowMs / 1000)}`;
headers["RateLimit-Remaining"] = String(remaining);
headers["RateLimit-Reset"] = String(retryAfterSeconds); // Remaining seconds
}
// 2. Legacy Headers (De facto Industry Standard - GitHub, Twitter, etc.)
if (config.legacyHeaders) {
headers["X-RateLimit-Limit"] = String(config.max);
headers["X-RateLimit-Remaining"] = String(remaining);
headers["X-RateLimit-Reset"] = String(Math.ceil(resetTime / 1000)); // Absolute Unix Epoch timestamp in seconds
}
if (isBlocked) {
headers["Retry-After"] = String(retryAfterSeconds);
return {
isBlocked: true,
statusCode: config.statusCode,
message: config.message,
headers
};
}
return {
isBlocked: false,
statusCode: 200,
message: "",
headers
};
}
blocks/rate-limiter/utils/ip.ts
IP extraction utility that checks x-forwarded-for, x-real-ip, then falls back to the socket remote address.
export function getClientIp(
headers: Record<string, string | string[] | undefined>,
remoteAddress?: string
): string {
// 1. Check standard proxy headers (if you trust your upstream proxy environment)
const xForwardedFor = headers["x-forwarded-for"];
if (xForwardedFor) {
const ips =
typeof xForwardedFor === "string" ? xForwardedFor.split(",") : xForwardedFor[0]!.split(",");
return ips[0]!.trim(); // The first IP is the actual client
}
const xRealIp = headers["x-real-ip"];
if (typeof xRealIp === "string") return xRealIp;
// 2. Fall back to raw socket address
return remoteAddress || "unknown-ip";
}
blocks/rate-limiter/adapters/express.ts
Express middleware factory that wraps the core engine.
import type { Request, Response, NextFunction } from "express";
import { evaluateRateLimit } from "../core/core";
import type { RateLimitStore, RateLimitConfig } from "../core/core";
import { getClientIp } from "../utils/ip";
export interface ExpressOptions extends Partial<RateLimitConfig> {
store: RateLimitStore;
keyGenerator?: (req: Request) => string;
}
export const expressRateLimit = (options: ExpressOptions) => {
const config: RateLimitConfig = {
windowMs: 60 * 1000,
max: 100,
statusCode: 429,
message: { error: "Too many requests, please try again later." },
standardHeaders: true,
legacyHeaders: true,
...options
};
const keyGen =
options.keyGenerator || ((req: Request) => getClientIp(req.headers, req.socket.remoteAddress));
return async (req: Request, res: Response, next: NextFunction): Promise<void> => {
try {
const key = keyGen(req);
const result = await evaluateRateLimit(key, options.store, config);
// Set computed rate limit headers dynamically
for (const [name, value] of Object.entries(result.headers)) {
res.setHeader(name, value);
}
if (result.isBlocked) {
res.status(result.statusCode).send(result.message);
return;
}
next();
} catch (error) {
// oxlint-disable-next-line no-console
console.error("Rate limiter failure (Fail-Open):", error);
next(); // Fail-open pattern intact
}
};
};
blocks/rate-limiter/adapters/fastify.ts
Fastify onRequest hook factory that wraps the core engine.
import type { FastifyRequest, FastifyReply } from "fastify";
import { evaluateRateLimit } from "../core/core";
import type { RateLimitStore, RateLimitConfig } from "../core/core";
import { getClientIp } from "../utils/ip";
export interface FastifyOptions extends Partial<RateLimitConfig> {
store: RateLimitStore;
keyGenerator?: (req: FastifyRequest) => string;
}
export const fastifyRateLimit = (options: FastifyOptions) => {
const config: RateLimitConfig = {
windowMs: 60 * 1000,
max: 100,
statusCode: 429,
message: { error: "Too many requests, please try again later." },
standardHeaders: true,
legacyHeaders: true,
...options
};
const keyGen =
options.keyGenerator ||
((req: FastifyRequest) => getClientIp(req.headers, req.socket.remoteAddress));
return async (request: FastifyRequest, reply: FastifyReply): Promise<void> => {
try {
const key = keyGen(request);
const result = await evaluateRateLimit(key, options.store, config);
reply.headers(result.headers);
if (result.isBlocked) {
reply.code(result.statusCode).send(result.message);
return; // Halts request lifecycle execution in Fastify hooks
}
} catch (error) {
request.log.error(error, "Rate limiter failure (Fail-Open)");
// Async hook implicit return continues execution automatically (Fail-Open)
}
};
};
blocks/rate-limiter/adapters/hono.ts
Hono middleware factory that wraps the core engine.
import { evaluateRateLimit } from "../core/core";
import type { RateLimitStore, RateLimitConfig } from "../core/core";
import { getClientIp } from "../utils/ip";
import type { ContentfulStatusCode } from "hono/utils/http-status";
import type { Context, MiddlewareHandler } from "hono";
export interface HonoOptions extends Partial<RateLimitConfig> {
store: RateLimitStore;
keyGenerator?: (c: Context) => string;
}
export const honoRateLimit = (options: HonoOptions): MiddlewareHandler => {
const config: RateLimitConfig = {
windowMs: 60 * 1000,
max: 100,
statusCode: 429,
message: { error: "Too many requests, please try again later." },
standardHeaders: true,
legacyHeaders: true,
...options
};
const keyGen =
options.keyGenerator ||
((c: Context) => getClientIp(Object.fromEntries(c.req.raw.headers.entries()), undefined));
return async (c, next) => {
try {
const key = keyGen(c);
const result = await evaluateRateLimit(key, options.store, config);
// Set computed rate limit headers dynamically
for (const [name, value] of Object.entries(result.headers)) {
c.header(name, value);
}
if (result.isBlocked) {
return c.json(result.message, result.statusCode as ContentfulStatusCode);
}
await next();
} catch (error) {
//oxlint-disable-next-line no-console
console.error("Rate limiter failure (Fail-Open):", error);
await next(); // Fail-open pattern intact
}
};
};
blocks/rate-limiter/variants/memory-store.ts
In-memory store using a Map with a chronological expiration queue and bounded incremental purge.
import type { RateLimitRecord, RateLimitStore } from "../core/core";
interface QueueEntry {
key: string;
resetTime: number;
}
export class MemoryStore implements RateLimitStore {
private readonly store = new Map<string, RateLimitRecord>();
// High-Performance Addition: A chronological queue tracking when keys expire
private readonly expirationQueue: QueueEntry[] = [];
private queueIndex = 0;
public constructor() {
// Periodically run a bounded incremental purge instead of a full loop
setInterval(() => this.purge(), 60 * 1000).unref();
}
public increment(key: string, windowMs: number): RateLimitRecord {
const now = Date.now();
const existing = this.store.get(key);
// 80/20 Optimization: Inline eviction if the key is already expired
if (existing && now > existing.resetTime) {
this.store.delete(key);
}
let record: RateLimitRecord;
if (existing && now <= existing.resetTime) {
record = {
hits: existing.hits + 1,
resetTime: existing.resetTime
};
} else {
record = {
hits: 1,
resetTime: now + windowMs
};
// Track this key's expiration chronologically
this.expirationQueue.push({ key, resetTime: record.resetTime });
}
this.store.set(key, record);
return { ...record };
}
private purge(): void {
const now = Date.now();
// Chronological Sweep: Since items are appended in order of expiration time,
// we only look at the oldest items at the front of our queue.
// The moment we hit an item that hasn't expired yet, we can STOP instantly.
while (this.queueIndex < this.expirationQueue.length) {
const entry = this.expirationQueue[this.queueIndex]!;
if (entry.resetTime > now) {
break; // Stop immediately. Everything after this is still valid.
}
// Evict from map if the current record matches this expiration mark
const currentRecord = this.store.get(entry.key);
if (currentRecord && currentRecord.resetTime <= now) {
this.store.delete(entry.key);
}
this.queueIndex++;
}
// Memory clean up for the queue array itself once it grows large
if (this.queueIndex > 10000) {
this.expirationQueue.splice(0, this.queueIndex);
this.queueIndex = 0;
}
}
}
blocks/rate-limiter/variants/redis-store.ts
Redis store using an atomic Lua script to increment and set expiry in one round-trip.
import type { Redis } from "ioredis";
import type { RateLimitStore, RateLimitRecord } from "../core/core";
export interface RedisWithRateLimit extends Redis {
performRateLimitIncrement(key: string, windowMs: number): Promise<[number, number]>;
}
export class RedisStore implements RateLimitStore {
constructor(
private redisClient: RedisWithRateLimit,
private keyPrefix = "rl:"
) {
// Define the atomic rate limit script inside ioredis once on startup
this.redisClient.defineCommand("performRateLimitIncrement", {
numberOfKeys: 1,
lua: `
local key = KEYS[1]
local windowMs = tonumber(ARGV[1])
-- 1. Increment the hit counter
local hits = redis.call('INCR', key)
-- 2. If it's a brand new key, immediately anchor its exact millisecond TTL
if hits == 1 then
redis.call('PEXPIRE', key, windowMs)
end
-- 3. Fetch the exact remaining millisecond TTL
local pttl = redis.call('PTTL', key)
return {hits, pttl}
`
});
}
async increment(key: string, windowMs: number): Promise<RateLimitRecord> {
const fullKey = `${this.keyPrefix}${key}`;
const now = Date.now();
// Execute the atomic script in exactly 1 network round-trip
// We cast to 'any' because we added a custom dynamic command name above
const [hits, pttl] = (await this.redisClient.performRateLimitIncrement(fullKey, windowMs)) as [
number,
number
];
// Handle defensive fallback if PTTL returns a negative error code
const actualRemainingMs = pttl > 0 ? pttl : windowMs;
return {
hits,
resetTime: now + actualRemainingMs
};
}
}
Configuration
RateLimitConfig
| Option | Type | Default | Description |
|---|---|---|---|
windowMs | number | 60000 | Time window in milliseconds |
max | number | 100 | Maximum requests allowed within the window |
statusCode | number | 429 | HTTP status code returned when rate limited |
message | string | Record<string, unknown> | { error: "Too many requests, please try again later." } | Response body when rate limited |
standardHeaders | boolean | true | Emit RFC-compliant RateLimit-* headers |
legacyHeaders | boolean | true | Emit X-RateLimit-* headers for backward compatibility |
Adapter Options
Each adapter accepts all RateLimitConfig fields plus:
| Option | Type | Default | Description |
|---|---|---|---|
store | RateLimitStore | Required | Storage backend implementation |
keyGenerator | (req) => string | Client IP (getClientIp) | Function that returns a unique client identifier |
The keyGenerator parameter type varies by adapter:
- Express:
(req: Request) => string - Fastify:
(req: FastifyRequest) => string - Hono:
(c: Context) => string
RedisStore
| Option | Type | Default | Description |
|---|---|---|---|
redisClient | RedisWithRateLimit | Required | ioredis client instance with the performRateLimitIncrement command defined |
keyPrefix | string | "rl:" | Prefix prepended to all Redis keys |
The MemoryStore is suitable for single-instance deployments only. Each server instance maintains
its own independent counter. Use RedisStore when you need a shared rate limit across multiple
instances.
Architecture
Request
│
▼
Adapter (Express / Fastify / Hono)
│ 1. Generate client key via keyGenerator
│ 2. Call evaluateRateLimit(key, store, config)
▼
evaluateRateLimit (core engine)
│ 1. store.increment(key, windowMs) → { hits, resetTime }
│ 2. Compute remaining quota and retry-after seconds
│ 3. Build standard + legacy headers
│ 4. Return { isBlocked, statusCode, message, headers }
▼
Adapter
│ 1. Apply headers to response
│ 2. If blocked → return 429 with message
│ 3. If allowed → pass through to next handler
│ 4. If store throws → log error, pass through (fail-open)The adapter extracts a client identifier (default: IP address), calls evaluateRateLimit with the key, store, and config, then applies the returned headers to the response. When the client exceeds the limit, the adapter returns a 429 Too Many Requests response with a Retry-After header. If the store throws an error, the adapter logs the failure and lets the request through — a store outage never blocks legitimate traffic.
The MemoryStore uses an in-memory Map with a chronological expiration queue. Expired entries are purged incrementally every 60 seconds, stopping at the first non-expired entry for O(1) average-case cleanup. The RedisStore uses a single atomic Lua script (INCR + PEXPIRE + PTTL) to increment the counter and read the remaining TTL in one network round-trip.
When to Use
- You need rate limiting across Express, Fastify, or Hono from a shared core engine.
- You want to swap between in-memory and Redis storage without changing application code.
- You want generated source code you own and can modify over an opaque npm dependency.
- You need RFC-compliant
RateLimit-*headers alongside legacyX-RateLimit-*headers.
When Not to Use
- You only use Express and prefer a zero-config solution like
express-rate-limit. - You do not need pluggable storage or cross-framework compatibility.
Usage
Express
import express from "express";
import { expressRateLimit } from "@/blocks/rate-limiter/adapters/express";
import { MemoryStore } from "@/blocks/rate-limiter/variants/memory-store";
const app = express();
// Apply rate limiting to all /api routes — 100 requests per minute
app.use(
"/api",
expressRateLimit({
windowMs: 60_000,
max: 100,
store: new MemoryStore()
})
);MemoryStore is for single-instance deployments only. Each server instance maintains its own
independent counter. Use RedisStore for shared rate limits across multiple instances.
Fastify
import Fastify from "fastify";
import { fastifyRateLimit } from "@/blocks/rate-limiter/adapters/fastify";
import { MemoryStore } from "@/blocks/rate-limiter/variants/memory-store";
const fastify = Fastify();
// onRequest hook runs before route handlers — 100 requests per minute
fastify.addHook(
"onRequest",
fastifyRateLimit({
windowMs: 60_000,
max: 100,
store: new MemoryStore()
})
);Hono
import { Hono } from "hono";
import { honoRateLimit } from "@/blocks/rate-limiter/adapters/hono";
import { MemoryStore } from "@/blocks/rate-limiter/variants/memory-store";
const app = new Hono();
// Apply to all routes — 100 requests per minute
app.use(
"*",
honoRateLimit({
windowMs: 60_000,
max: 100,
store: new MemoryStore()
})
);Custom Key Generator
Use the authenticated user's ID instead of IP to rate-limit per account:
import { expressRateLimit } from "@/blocks/rate-limiter/adapters/express";
import { MemoryStore } from "@/blocks/rate-limiter/variants/memory-store";
app.use(
"/api",
expressRateLimit({
windowMs: 60_000,
max: 100,
store: new MemoryStore(),
// Rate limit by user ID instead of IP — prevents shared-IP false positives
keyGenerator(req) {
return req.user?.id ?? "anonymous";
}
})
);Production Usage with Redis
Shared rate limit across multiple server instances:
import Redis from "ioredis";
import { expressRateLimit } from "@/blocks/rate-limiter/adapters/express";
import { RedisStore } from "@/blocks/rate-limiter/variants/redis-store";
const redis = new Redis(process.env.REDIS_URL!);
app.use(
"/api",
expressRateLimit({
windowMs: 60_000,
max: 100,
// Atomic Lua script — increment + expire in one round-trip
store: new RedisStore(redis)
})
);Custom Store
Implement the RateLimitStore interface to plug in any backing store:
import type { RateLimitStore, RateLimitRecord } from "@/blocks/rate-limiter/core/core";
class PostgresStore implements RateLimitStore {
async increment(key: string, windowMs: number): Promise<RateLimitRecord> {
const result = await db.query(
`INSERT INTO rate_limits (key, hits, reset_time)
VALUES ($1, 1, NOW() + $2::interval)
ON CONFLICT (key) DO UPDATE SET hits = rate_limits.hits + 1
RETURNING hits, reset_time`,
[key, `${windowMs} milliseconds`]
);
return {
hits: result.rows[0].hits,
resetTime: new Date(result.rows[0].reset_time).getTime()
};
}
}API Reference
evaluateRateLimit
Core engine that checks and increments the rate limit for a given key.
async function evaluateRateLimit(
key: string,
store: RateLimitStore,
config: RateLimitConfig
): Promise<RateLimitResult>;| Parameter | Type | Required | Description |
|---|---|---|---|
key | string | Yes | Unique client identifier |
store | RateLimitStore | Yes | Storage backend to persist counters |
config | RateLimitConfig | Yes | Rate limit configuration |
Returns — RateLimitResult with isBlocked, statusCode, message, and headers.
expressRateLimit
Creates Express middleware for rate limiting.
function expressRateLimit(
options: ExpressOptions
): (req: Request, res: Response, next: NextFunction) => Promise<void>;| Parameter | Type | Required | Description |
|---|---|---|---|
options | ExpressOptions | Yes | Configuration with required store field |
fastifyRateLimit
Creates a Fastify onRequest hook for rate limiting.
function fastifyRateLimit(
options: FastifyOptions
): (request: FastifyRequest, reply: FastifyReply) => Promise<void>;| Parameter | Type | Required | Description |
|---|---|---|---|
options | FastifyOptions | Yes | Configuration with required store field |
honoRateLimit
Creates Hono middleware for rate limiting.
function honoRateLimit(options: HonoOptions): MiddlewareHandler;| Parameter | Type | Required | Description |
|---|---|---|---|
options | HonoOptions | Yes | Configuration with required store field |
getClientIp
Extracts the client IP from request headers. Checks x-forwarded-for, x-real-ip, then falls back to remoteAddress.
function getClientIp(
headers: Record<string, string | string[] | undefined>,
remoteAddress?: string
): string;| Parameter | Type | Required | Description |
|---|---|---|---|
headers | Record<string, string | string[] | undefined> | Yes | Request headers object |
remoteAddress | string | No | Socket remote address fallback |
Returns — The first IP from x-forwarded-for, or x-real-ip, or remoteAddress, or "unknown-ip".
MemoryStore
In-memory store using a Map with a chronological expiration queue and bounded incremental purge every 60 seconds.
class MemoryStore implements RateLimitStore {
increment(key: string, windowMs: number): RateLimitRecord;
}RedisStore
Redis store using an atomic Lua script to increment and set expiry in one network round-trip.
class RedisStore implements RateLimitStore {
constructor(redisClient: RedisWithRateLimit, keyPrefix?: string);
increment(key: string, windowMs: number): Promise<RateLimitRecord>;
}RateLimitConfig
interface RateLimitConfig {
windowMs: number;
max: number;
statusCode: number;
message: string | Record<string, unknown>;
standardHeaders: boolean;
legacyHeaders: boolean;
}RateLimitStore
interface RateLimitStore {
increment(key: string, windowMs: number): Promise<RateLimitRecord> | RateLimitRecord;
}RateLimitRecord
interface RateLimitRecord {
hits: number;
resetTime: number; // Unix timestamp in ms
}RateLimitResult
interface RateLimitResult {
isBlocked: boolean;
statusCode: number;
message: string | Record<string, unknown>;
headers: Record<string, string>;
}Examples
Basic Usage
Minimal setup — 100 requests per minute with in-memory storage:
import { expressRateLimit } from "@/blocks/rate-limiter/adapters/express";
import { MemoryStore } from "@/blocks/rate-limiter/variants/memory-store";
app.use(
expressRateLimit({
store: new MemoryStore(),
max: 100,
windowMs: 60_000
})
);Production Usage with Redis
Shared counter across instances with Redis:
import Redis from "ioredis";
import { expressRateLimit } from "@/blocks/rate-limiter/adapters/express";
import { RedisStore } from "@/blocks/rate-limiter/variants/redis-store";
const redis = new Redis(process.env.REDIS_URL!);
app.use(
"/api",
expressRateLimit({
max: 100,
windowMs: 60_000,
store: new RedisStore(redis)
})
);Per-User Rate Limiting
Use an authenticated user ID as the key instead of IP:
expressRateLimit({
max: 50,
windowMs: 60_000,
store: new MemoryStore(),
keyGenerator(req) {
return req.user.id; // Rate limit per account, not per IP
}
});Testing
Unit-test rate limiting by mocking the store:
import { evaluateRateLimit } from "@/blocks/rate-limiter/core/core";
import type { RateLimitStore, RateLimitRecord } from "@/blocks/rate-limiter/core/core";
// Mock store that returns a specific hit count
function createMockStore(hits: number): RateLimitStore {
return {
increment: () => ({
hits,
resetTime: Date.now() + 60_000
})
};
}
const config = {
windowMs: 60_000,
max: 100,
statusCode: 429,
message: { error: "Too many requests" },
standardHeaders: true,
legacyHeaders: true
};
// Under limit — request passes through
const allowed = await evaluateRateLimit("user-1", createMockStore(50), config);
console.assert(allowed.isBlocked === false);
// Over limit — request blocked with 429
const blocked = await evaluateRateLimit("user-1", createMockStore(101), config);
console.assert(blocked.isBlocked === true);
console.assert(blocked.statusCode === 429);FAQ
What happens when the store throws an error?
The adapter logs the error and calls next() (or await next()), allowing the request to proceed. This fail-open behavior prevents a store outage from blocking legitimate traffic.
How do the standard and legacy headers differ?
Standard headers (RateLimit-*) follow the RFC 9421 draft. RateLimit-Reset contains the remaining seconds in the window. Legacy headers (X-RateLimit-*) match the de facto industry standard (GitHub, Twitter). X-RateLimit-Reset contains an absolute Unix epoch timestamp in seconds. Both are enabled by default.
What does the RateLimit-Limit header format look like?
The standard header uses the format 100, window=60 — the max count followed by the window duration in seconds.
Can I use both standard and legacy headers?
Yes. Both are enabled by default. Toggle them independently with standardHeaders and legacyHeaders.
How does the MemoryStore handle expiration?
Expired entries are purged incrementally every 60 seconds. The store maintains a chronological queue of expiration times. During purge, it walks from the oldest entry and stops at the first non-expired item — so cleanup is O(1) amortized, not a full map scan.
How does the RedisStore stay atomic?
A single Lua script executes INCR, PEXPIRE (on first hit), and PTTL in one round-trip. This prevents race conditions where two concurrent requests could both see hits === 1 and double-set the expiry.
MCP Server
Connect Blockend to your AI coding assistant through the Model Context Protocol (MCP) to discover, analyze, and install backend blocks directly into your projects.
Error Handler
Centralized, type-safe error handling pipeline for Express applications with a typed AppError class and error catalog.