Skip to content

Rate Limiting

Rate limiting in @guardcore supports multiple granularity levels, atomic Redis operations via Lua scripts, and automatic in-memory fallback when Redis is unavailable.

Applies to all endpoints:

const config: SecurityConfig = {
enableRateLimiting: true,
rateLimit: 100,
rateLimitWindow: 60,
};

Every IP is limited to 100 requests per 60-second sliding window.

Override limits for specific URL paths:

const config: SecurityConfig = {
rateLimit: 100,
rateLimitWindow: 60,
endpointRateLimits: {
'/api/login': [5, 300],
'/api/search': [20, 60],
'/api/upload': [3, 60],
},
};

The tuple is [maxRequests, windowSeconds]. Endpoint limits take priority over global limits.

Apply rate limits to individual route handlers using decorators:

import { SecurityDecorator, SecurityConfigSchema } from '@guardcore/core';
const config = SecurityConfigSchema.parse({ enableRedis: false });
const guard = new SecurityDecorator(config);
const handler = guard.rateLimit(5, 60)(async (req) => {
return { data: 'limited endpoint' };
});

Different limits per country code (requires geoIpHandler or geoResolver):

const handler = guard.geoRateLimit({
US: [100, 60],
CN: [10, 60],
DEFAULT: [50, 60],
})(async (req) => {
return { data: 'geo-limited' };
});

When Redis is available, rate limiting uses an atomic Lua script for accuracy under concurrency:

local key = KEYS[1]
local now = tonumber(ARGV[1])
local window = tonumber(ARGV[2])
local limit = tonumber(ARGV[3])
local window_start = now - window
redis.call('ZADD', key, now, now)
redis.call('ZREMRANGEBYSCORE', key, 0, window_start)
local count = redis.call('ZCARD', key)
redis.call('EXPIRE', key, window * 2)
return count

This uses a Redis sorted set where each request timestamp is a member scored by its time. Old entries are pruned, and the count is returned atomically.

The script is loaded once via SCRIPT LOAD and executed with EVALSHA for performance.

When Redis is unavailable, rate limiting automatically falls back to an in-memory Map<string, number[]> that tracks timestamps per key. This works correctly for single-process deployments but does not share state across processes.

The fallback activates when:

  • enableRedis is false
  • Redis connection fails at startup
  • A Redis operation fails at runtime (per-request fallback)
{prefix}rate_limit:rate:{ip}:{endpoint}

Example: guard_core:rate_limit:rate:192.168.1.1:/api/login

Value type: Sorted set (timestamps as scores) TTL: window * 2 seconds

When a client exceeds their limit, the middleware returns:

{
"detail": "Rate limit exceeded"
}

Status code: 429 Too Many Requests

FieldTypeDefaultDescription
enableRateLimitingbooleantrueEnable/disable rate limiting
rateLimitnumber10Global max requests per window
rateLimitWindownumber60Global window in seconds
endpointRateLimitsRecord<string, [number, number]>{}Per-endpoint [limit, window]