High-Concurrency API Rate Limiting Using Redis Generic Cell Rate Algorithm
Protect API endpoints from abuse using the Generic Cell Rate Algorithm (GCRA) and Redis/Valkey, delivering millisecond enforcement and RFC 7807 headers.
Netbay Developer Relations
Netbay Engineering
On this page
Rate limiting is an essential defense against brute-force authentication attacks, denial-of-service attempts, and runaway scraping scripts. While simple fixed-window rate limiters (e.g., INCR and EXPIRE keys per minute) are straightforward to implement, they suffer from boundary bursts: a client can exhaust their entire quota in the final second of minute one and immediately exhaust another full quota in the first second of minute two, generating a 2x traffic spike.
The Generic Cell Rate Algorithm (GCRA)—an implementation of the leaky bucket algorithm originally standardized in ATM telecommunications—solves this problem. By tracking a single theoretical arrival time (TAT) per client, GCRA enforces smooth rate limiting with sub-millisecond overhead and atomic Lua scripts in Valkey or Redis.
How GCRA Works Under the Hood
Unlike token bucket implementations that track both a token count and a last-refilled timestamp, GCRA tracks a single value: the Theoretical Arrival Time (TAT).
- When a request arrives, GCRA compares the current time (now) with the stored TAT.
- If now is past TAT, the bucket is completely empty; the new TAT is set to now + emission_interval.
- If now is before TAT, the request is consuming burst allowance. As long as TAT - now is less than the max_burst_limit, the request is permitted, and TAT is incremented by emission_interval.
- If TAT - now exceeds max_burst_limit, the request is rejected with HTTP 429 Too Many Requests.
This approach provides smooth continuous pacing rather than abrupt cliff-edge drops.
Atomic GCRA Lua Script in Valkey
Because rate-limiting checks must be atomic across concurrent application threads, execute GCRA logic directly inside Valkey using an optimized Lua script:
-- gcra.lua
-- KEYS[1]: Rate limit key (e.g. rate:user:12345)
-- ARGV[1]: Max burst capacity (e.g. 10 requests)
-- ARGV[2]: Emission interval in seconds (e.g. 1 second per request)
-- ARGV[3]: Current epoch timestamp in seconds
local key = KEYS[1]
local max_burst = tonumber(ARGV[1])
local emission_interval = tonumber(ARGV[2])
local now = tonumber(ARGV[3])
local burst_offset = max_burst * emission_interval
local tat = redis.call('GET', key)
if not tat then
tat = now
else
tat = tonumber(tat)
end
local new_tat = math.max(tat, now) + emission_interval
local allow_at = new_tat - burst_offset
if allow_at <= now then
redis.call('SET', key, new_tat, 'EX', math.ceil(burst_offset + emission_interval))
local remaining = math.floor((burst_offset - (new_tat - now)) / emission_interval)
return { 1, math.max(0, remaining), math.ceil(new_tat - now) }
else
local retry_after = math.ceil(allow_at - now)
return { 0, 0, retry_after }
endThe script returns an array with three values:
- Allowed flag (1 for allowed, 0 for blocked).
- Remaining tokens in the current burst window.
- Reset/Retry-After seconds until quota becomes available.
Express / Node.js Middleware Implementation
Integrate the GCRA Lua script into an API gateway or web framework:
// rate-limit-middleware.ts
import { Request, Response, NextFunction } from "express";
import Redis from "ioredis";
import { readFileSync } from "node:fs";
const redis = new Redis("redis://10.0.0.4:6379");
const gcraScript = readFileSync("./gcra.lua", "utf8");
export function rateLimiter(capacity = 30, intervalSeconds = 1) {
return async (req: Request, res: Response, next: NextFunction) => {
const clientId = req.headers["x-api-key"] || req.ip;
const key = "ratelimit:" + clientId;
const now = Date.now() / 1000;
try {
const [allowed, remaining, resetTime] = (await redis.eval(
gcraScript,
1,
key,
capacity,
intervalSeconds,
now
)) as [number, number, number];
// Standard IETF RateLimit Headers
res.setHeader("RateLimit-Limit", capacity);
res.setHeader("RateLimit-Remaining", remaining);
res.setHeader("RateLimit-Reset", resetTime);
if (allowed === 1) {
return next();
}
res.setHeader("Retry-After", resetTime);
return res.status(429).json({
type: "https://example.com/errors/rate-limit-exceeded",
title: "Too Many Requests",
status: 429,
detail: "API quota exceeded. Retry in " + resetTime + " seconds.",
retry_after: resetTime,
});
} catch (err) {
console.error("Rate limiter error, failing open:", err);
return next();
}
};
}Benchmarking Rate Enforcement
Test rate limiter throughput and verify HTTP 429 response formatting using curl or load generators:
# Send rapid requests in succession
for i in {1..35}; do
curl -s -o /dev/null -w "%{http_code} RateLimit-Remaining: %{header_json}
" http://localhost:3000/api/data
doneRequests 1 to 30 return HTTP 200 OK, while requests 31 to 35 immediately return HTTP 429 Too Many Requests with accurate Retry-After values.
Deploy high-throughput Valkey and API microservices on Netbay Linux VPS in under 60 seconds with High-Speed SSD in Lucknow DC01 — netbayhosts.in.
Keep reading
Follow along on a real VPS
Deploy Linux in under 60 seconds
These guides are written against Ubuntu, Debian, and RHEL-family images — the same ones on NetBay.
Deploy an instance