Skip to content

[BUG] AI Insights in-memory rate limiter resets on serverless cold starts, making it bypassable #2212

@nyxsky404

Description

@nyxsky404

Describe the bug

The `/api/ai-insights` endpoint enforces a 5-requests-per-hour rate limit using a module-level `Map`:

```ts
// src/app/api/ai-insights/route.ts
const aiInsightsRateLimit = new Map<
string,
{ count: number; resetTime: number }

();
```

In serverless environments (Vercel, AWS Lambda), each function invocation may run in a fresh container. The `Map` is reset on every cold start. A user can exhaust their 5-request quota, wait 30–60 seconds for the function container to expire, and then make 5 more requests — repeating indefinitely. The rate limit provides no real protection.

Why this matters

The AI Insights endpoint calls either the Anthropic API (`ANTHROPIC_API_KEY`) or the Groq API (`GROQ_API_KEY`). Both are metered services billed per token. Unbounded access through rate-limit bypass translates directly into unexpected API costs for the project.

Contrast with other rate-limited endpoints

Every other rate-limited endpoint in the codebase uses the Upstash Redis client with atomic Lua scripts, which is durable across cold starts:

  • `src/lib/auth-rate-limit.ts` — auth endpoints
  • `src/middleware.ts` — metrics endpoints (60 req/min per user)
  • `src/lib/contact-rate-limit.ts` — contact form (3 req/hour per IP)

The AI Insights endpoint is the only one that uses an in-memory `Map` as its primary (and only) rate-limiting mechanism.

Files affected

  • `src/app/api/ai-insights/route.ts` (lines 12–16, 97–120) — in-memory rate limiter definition and usage

Expected behaviour

The rate limit should persist across serverless function instances. A user who makes 5 requests should be blocked for the remainder of the hour regardless of cold starts or concurrent instances.

Suggested fix

Replace the in-memory `Map` with the Upstash Redis rate limiter already integrated in `src/lib/rate-limit.ts`. The fix is a drop-in replacement following the exact pattern used by existing rate-limited routes:

```ts
// Before the AI generation logic, replace the Map-based check with:
import { rateLimit } from "@/lib/rate-limit";

const rateLimitResult = await rateLimit(ai-insights:${userId}, {
max: 5,
windowSeconds: 60 * 60, // 1 hour
});

if (!rateLimitResult.success) {
return NextResponse.json(
{ error: "Rate limit exceeded. Try again later." },
{
status: 429,
headers: { "Retry-After": String(rateLimitResult.retryAfter) },
}
);
}
```

The module-level `aiInsightsRateLimit` Map and all related `existing.count` logic can then be removed entirely.

Metadata

Metadata

Assignees

Labels

gssoc:assignedGSSoC: Issue assigned to a contributor

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions