User story
As an operator, I want public API routes rate-limited by IP, so that a single user cannot exhaust server resources by spamming carousel generation requests (which each spin up a Puppeteer browser).
Background
/api/generate-carousel and /api/extract-captions (and /api/transcribe, the backend for /get-youtube-captions) have no rate limiting. Each carousel generation request launches a full Puppeteer browser instance. Without a limit, one user can make dozens of requests in parallel, exhausting the server's memory and CPU. The app is deployed on a container host (not Vercel serverless), so in-memory rate limiting is sufficient — no Redis required.
/api/auto-carousel is pending deletion (Issue #76) and is excluded from scope.
Acceptance criteria
Happy path
Given a user sends 5 carousel generate requests within 60 seconds from the same IP
When each request arrives
Then all 5 are processed normally and return 200
Error path
Given a user sends their 6th generate-carousel request within 60 seconds from the same IP
When the request arrives
Then the server returns 429 { error: "Too many requests. Please wait before trying again." } and does not start a Puppeteer browser
Given 60 seconds have passed since the user's first request
When they send a new request
Then the limit resets and the request is processed normally
Edge case
Given a request comes from behind a load balancer or proxy
When the rate limiter reads the IP
Then it uses X-Forwarded-For if present, falling back to req.ip, so the real client IP is used
Out of scope
- Redis-based rate limiting
- Per-user (authenticated) rate limits
- Rate limiting /api/auth/subscribe (handled separately if needed)
- /api/editor/* and /api/camera/* internal routes
Technical context
Library: node-rate-limiter-flexible (npm package, no external dependency).
In-memory store is sufficient for a single-container deployment. If the app is scaled to multiple containers in the future, the store can be swapped to Redis without changing the API surface.
Rate limit proposal (adjust as needed before implementation):
/api/generate-carousel: 5 requests per 60 seconds per IP
/api/extract-captions: 20 requests per 60 seconds per IP
/api/transcribe: 20 requests per 60 seconds per IP
Files:
app/lib/rateLimiter.ts — new: shared RateLimiterMemory instances and helper
app/api/generate-carousel/route.ts — add rate limit check
app/api/extract-captions/route.ts — add rate limit check
app/api/transcribe/route.ts — add rate limit check
Implementation details
-
Create app/lib/rateLimiter.ts
- Export one RateLimiterMemory instance per route (or a factory keyed by route name)
- Helper:
async function checkRateLimit(req: NextRequest, limiter): Promise<Response | null>
- Returns a 429 NextResponse if over limit, null if OK
- Extract IP from
req.headers.get('x-forwarded-for') ?? req.ip ?? 'unknown'
-
In each API route, add at the top of the handler:
const limit = await checkRateLimit(request, generateCarouselLimiter);
if (limit) return limit;
-
429 response format: { error: "Too many requests. Please wait before trying again." }
Additional test scenarios
- Two different IPs can each make 5 requests without interfering with each other
- The 6th request from one IP does not affect the other IP's remaining allowance
- Rate limit resets correctly after the window expires
Hard constraints
- No Redis dependency — in-memory store only for this issue
- 429 response must be JSON, not HTML
- Rate limit state must not cause interference between different routes
Dependency issues
User story
As an operator, I want public API routes rate-limited by IP, so that a single user cannot exhaust server resources by spamming carousel generation requests (which each spin up a Puppeteer browser).
Background
/api/generate-carousel and /api/extract-captions (and /api/transcribe, the backend for /get-youtube-captions) have no rate limiting. Each carousel generation request launches a full Puppeteer browser instance. Without a limit, one user can make dozens of requests in parallel, exhausting the server's memory and CPU. The app is deployed on a container host (not Vercel serverless), so in-memory rate limiting is sufficient — no Redis required.
/api/auto-carousel is pending deletion (Issue #76) and is excluded from scope.
Acceptance criteria
Happy path
Given a user sends 5 carousel generate requests within 60 seconds from the same IP
When each request arrives
Then all 5 are processed normally and return 200
Error path
Given a user sends their 6th generate-carousel request within 60 seconds from the same IP
When the request arrives
Then the server returns 429
{ error: "Too many requests. Please wait before trying again." }and does not start a Puppeteer browserGiven 60 seconds have passed since the user's first request
When they send a new request
Then the limit resets and the request is processed normally
Edge case
Given a request comes from behind a load balancer or proxy
When the rate limiter reads the IP
Then it uses X-Forwarded-For if present, falling back to req.ip, so the real client IP is used
Out of scope
Technical context
Library:
node-rate-limiter-flexible(npm package, no external dependency).In-memory store is sufficient for a single-container deployment. If the app is scaled to multiple containers in the future, the store can be swapped to Redis without changing the API surface.
Rate limit proposal (adjust as needed before implementation):
Files:
app/lib/rateLimiter.ts— new: shared RateLimiterMemory instances and helperapp/api/generate-carousel/route.ts— add rate limit checkapp/api/extract-captions/route.ts— add rate limit checkapp/api/transcribe/route.ts— add rate limit checkImplementation details
Create
app/lib/rateLimiter.tsasync function checkRateLimit(req: NextRequest, limiter): Promise<Response | null>req.headers.get('x-forwarded-for') ?? req.ip ?? 'unknown'In each API route, add at the top of the handler:
429 response format:
{ error: "Too many requests. Please wait before trying again." }Additional test scenarios
Hard constraints
Dependency issues