Skip to content

feat: Redis-backed playground rate limiting with session cookies (#93)#102

Merged
DevanshuNEU merged 4 commits into
OpenCodeIntel:mainfrom
DevanshuNEU:feat/issue-93-playground-rate-limiting
Dec 13, 2025
Merged

feat: Redis-backed playground rate limiting with session cookies (#93)#102
DevanshuNEU merged 4 commits into
OpenCodeIntel:mainfrom
DevanshuNEU:feat/issue-93-playground-rate-limiting

Conversation

@DevanshuNEU

Copy link
Copy Markdown
Collaborator

Summary

Fixes the critical security issue where playground rate limits reset on page refresh, exposing API costs.

Problem

  • Rate limit was stored in React state only → reset on refresh
  • Backend used in-memory dict → reset on server restart
  • Users could get unlimited free searches by simply refreshing the browser
  • Each search = OpenAI embedding API call = $$$

Solution

Implemented a 3-layer Redis-backed rate limiting system:

Layer Purpose Limit
Session token Per-device tracking via httpOnly cookie 50/day
IP fallback Shared network protection 100/day
Global circuit breaker Cost protection during attacks 10,000/hour

Architecture

┌─────────────────────────────────────────────┐
│  Frontend (source of truth = backend)       │
│  • Fetch limits on mount                    │
│  • Use backend response for remaining       │
│  • Include credentials for cookies          │
└─────────────────────────────────────────────┘
                    │
                    ▼
┌─────────────────────────────────────────────┐
│  Backend                                    │
│  • PlaygroundLimiter service (Redis-backed) │
│  • httpOnly session cookie                  │
│  • GET /playground/limits endpoint          │
│  • GET /playground/stats for monitoring     │
└─────────────────────────────────────────────┘
                    │
                    ▼
┌─────────────────────────────────────────────┐
│  Redis                                      │
│  • playground:session:{token} → count       │
│  • playground:ip:{hash} → count             │
│  • playground:global:hourly → count         │
└─────────────────────────────────────────────┘

Why These Design Decisions?

Decision Rationale
Session token over IP-only IP punishes shared networks (offices, universities)
httpOnly cookie User can't clear it with localStorage.clear()
50/day not 5 Generous limits = better conversion. Hook users, don't frustrate them
IP as fallback Catches incognito users and bots that reject cookies
Global circuit breaker If attacked with 1000 IPs, we still have a ceiling
Fail-open if Redis down Better UX than blocking (rare edge case)

New Files

  • backend/services/playground_limiter.py - Redis-backed rate limiter service

Modified Files

  • backend/routes/playground.py - Use new limiter, add cookie handling
  • backend/dependencies.py - Export redis_client
  • frontend/src/pages/LandingPage.tsx - Use backend as source of truth
  • frontend/src/pages/Playground.tsx - Use backend as source of truth

New API Endpoints

GET /api/v1/playground/limits
Response: {
  "remaining": 47,
  "limit": 50,
  "resets_at": "2024-12-14T00:00:00Z",
  "tier": "anonymous"
}

GET /api/v1/playground/stats  (for monitoring)
Response: {
  "global_hourly": 156,
  "global_limit": 10000,
  "redis_available": true
}

Testing

  • ✅ 78 backend tests pass
  • ✅ Frontend builds successfully
  • ✅ Manual integration testing verified:
    • Session persists across requests
    • Remaining count decrements correctly
    • New session gets fresh 50 limit
    • Redis tracks all layers (session, IP, global)
    • Cookie is set as httpOnly

Security Considerations

  • Cookie is httpOnly (can't be accessed by JavaScript)
  • Cookie is secure in production (HTTPS only)
  • Cookie is samesite=lax (CSRF protection)
  • IP addresses are hashed before storage (privacy)
  • Fail-open design if Redis unavailable (availability > strictness)

Closes #93

- Session-based limiting (50/day per device via httpOnly cookie)
- IP-based fallback (100/day for shared networks)
- Global circuit breaker (10k/hour for cost protection)
- Fail-open design if Redis unavailable
- PlaygroundLimitResult dataclass for structured responses

Part of OpenCodeIntel#93
- Replace in-memory rate limiting with Redis-backed limiter
- Add GET /playground/limits endpoint for frontend to check remaining
- Add GET /playground/stats for monitoring
- Set httpOnly session cookie on first request
- Export redis_client from dependencies
- Python 3.9 compatible type hints

Part of OpenCodeIntel#93
- Fetch limits on mount from GET /playground/limits
- Include credentials for session cookie tracking
- Use backend response as source of truth for remaining count
- Handle 429 rate limit errors with user-friendly message
- Remove client-side only tracking (was bypassable on refresh)
- Update both LandingPage.tsx and Playground.tsx

Part of OpenCodeIntel#93
- Add IS_PRODUCTION flag from ENVIRONMENT env var
- Set secure=True for cookie only in production (requires HTTPS)
- Development uses secure=False for localhost testing

Part of OpenCodeIntel#93
@vercel

vercel Bot commented Dec 13, 2025

Copy link
Copy Markdown

@DevanshuNEU is attempting to deploy a commit to the Dev's projects Team on Vercel.

A member of the Team first needs to authorize it.

@vercel

vercel Bot commented Dec 13, 2025

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Review Updated (UTC)
opencodeintel Ignored Ignored Preview Dec 13, 2025 9:55pm

@DevanshuNEU DevanshuNEU merged commit 622a852 into OpenCodeIntel:main Dec 13, 2025
6 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

security(playground): Rate limit resets on page refresh - API cost exposure

1 participant