thedotmack/claude-mem · warning
rate_limited
rate_limited
Error message
Rate limit exceeded (${opts.max} requests / ${opts.windowSec}s) What it means
429 from the fixed-window rate limiter: the subject (per API key id) exceeded opts.max requests inside the current opts.windowSec window. The middleware sets X-RateLimit-Limit, X-RateLimit-Remaining, and Unix-seconds X-RateLimit-Reset on every response, plus Retry-After (seconds until window reset) when it rejects — clients should schedule retries from those headers, not guess.
Source
Thrown at src/server/middleware/rate-limit.ts:42
async function enforceRateLimit(
repo: PostgresRateLimitRepository,
opts: { windowSec: number; max: number },
subject: string,
res: Response,
next: NextFunction,
): Promise<Response | void> {
const start = floorToWindow(Date.now(), opts.windowSec);
const resetMs = start.getTime() + opts.windowSec * 1000;
const result = await repo.hit({ subjectId: subject, windowStart: start, limit: opts.max });
res.setHeader('X-RateLimit-Limit', String(opts.max));
res.setHeader('X-RateLimit-Remaining', String(Math.max(0, opts.max - result.count)));
// Unix-seconds reset time — the conventional companion to Retry-After that
// most client libraries read to schedule automatic retries.
res.setHeader('X-RateLimit-Reset', String(Math.ceil(resetMs / 1000)));
if (!result.allowed) {
const retryAfter = Math.max(1, Math.ceil((resetMs - Date.now()) / 1000));
res.setHeader('Retry-After', String(retryAfter));
return res.status(429).json({
error: 'rate_limited',
message: `Rate limit exceeded (${opts.max} requests / ${opts.windowSec}s)`,
});
}
return next();
}
/** Fixed-window per-key limiter: `max` requests per `windowSec`. */
export function requireRateLimit(pool: PostgresPool, opts: { windowSec: number; max: number }): RequestHandler {
const repo = new PostgresRateLimitRepository(pool);
return async (req: Request, res: Response, next: NextFunction) => {
const subject = req.authContext?.apiKeyId;
if (!subject) return next(); // unauthenticated / local-dev bypass: nothing to limit
try {
return await enforceRateLimit(repo, opts, subject, res, next);
} catch (error) {
logger.warn('HTTP', 'rate limit check failed; allowing request (fail open)', {
error: error instanceof Error ? error.message : String(error),View on GitHub (pinned to e2d1df569a)
Solutions
- Honor the Retry-After header: wait that many seconds (plus jitter) before the next request.
- Switch to POST /v1/events/batch (up to 500 events per call) to collapse many hits into one.
- Give each worker its own API key so limits are per-key, not shared.
- If the cap is genuinely too low for legit traffic, raise the limiter's max/windowSec configuration server-side.
Example fix
// before: naive loop trips 429
for (const e of events) await post('/v1/events', e);
// after: batch + honor Retry-After
async function postWithRetry(url, body) {
const res = await fetch(url, { method: 'POST', headers, body: JSON.stringify(body) });
if (res.status === 429) {
const wait = Number(res.headers.get('retry-after') ?? 1);
await new Promise(r => setTimeout(r, (wait + Math.random()) * 1000));
return postWithRetry(url, body);
}
return res;
}
await postWithRetry(`${base}/v1/events/batch`, events.slice(0, 500)); Defensive patterns
Strategy: retry
Validate before calling
// Pre-check remaining budget from the last response's headers
function canSend(rateState: { remaining: number }): boolean {
return rateState.remaining > 0;
} Type guard
interface RateLimitBody { error: string; message: string }
function isRateLimited(res: Response): boolean {
return res.status === 429;
} Try / catch
async function fetchRateLimited(url: string, init: RequestInit, tries = 5): Promise<Response> {
const res = await fetch(url, init);
if (res.status !== 429) return res;
if (tries === 0) throw new Error('rate limit exhausted retries');
const retryAfter = Number(res.headers.get('retry-after') ?? 1);
const reset = res.headers.get('x-ratelimit-reset');
await new Promise(r => setTimeout(r, (retryAfter + Math.random()) * 1000));
return fetchRateLimited(url, init, tries - 1);
} Prevention
- Always read Retry-After / X-RateLimit-* headers; never poll on a fixed interval.
- Prefer the batch endpoint (500 events/request) over per-event calls.
- Give each concurrent worker its own API key.
- Add jitter to delays to avoid thundering herd at window reset.
When it happens
Trigger: Burst of N+1 requests from one API key within windowSec (e.g. >max POST /v1/events in a minute); a retry loop without backoff re-tripping the window; many worker processes sharing one key so their combined traffic exceeds the per-key cap.
Common situations: Bulk backfill scripts hammering the events endpoint sequentially instead of batching; fan-out from parallel agents sharing a team key; window boundary causing a burst right after reset.
Related errors
AI-assisted analysis of thedotmack/claude-mem@e2d1df569a (2026-08-20).
Data as JSON: /api/errors/a9bb744e28f5ab56.
Report an issue: GitHub.