Skip to content

feat(api): add IP rate limiting to public carousel and youtube-captions API routes #78

@natashaannn

Description

@natashaannn

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

  1. 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'
  2. In each API route, add at the top of the handler:

    const limit = await checkRateLimit(request, generateCarouselLimiter);
    if (limit) return limit;
  3. 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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions