# Redis Rate Limiting Implementation Guide for $50 App

## Executive Summary

The current `lib/rate-limit.ts` uses in-memory `Map()` which resets per serverless invocation on Vercel. This won't work at scale in production. We need a distributed, persistent rate limiting solution.

**Primary Recommendation: Upstash Redis** - The standard solution for Vercel/Next.js applications with excellent free tier and seamless integration.

## 1. Upstash Redis Solution

### Pricing & Free Tier Limits

**Free Tier (Perfect for development & moderate traffic):**
- **256MB data size** - More than enough for rate limiting (keys expire quickly)
- **500K commands per month** - Each rate limit check = ~2-3 commands
- **One free database per account**
- **10GB monthly bandwidth** included
- **10,000 requests per second** limit

**Pay-as-you-go (Scales with usage):**
- **$0.20 per 100K requests** (commands)
- **First 200GB bandwidth free**, then $0.03/GB
- **$0.25/GB storage** (first 1GB free)
- **Budget controls** available to prevent unexpected charges

**Fixed Plans (Predictable costs):**
- Starting at $10/month for 250MB
- Unlimited requests included
- Bandwidth and storage included

**For the $50 app:** The free tier should be sufficient for initial launch. At 500K commands/month, that's ~166K rate limit checks (assuming 3 commands/check). Even with 100 active users making 100 requests/day each, that's only 10K checks/day = 300K/month.

### Setup Process

1. **Create Upstash Redis Database:**
   - Go to [console.upstash.com](https://console.upstash.com)
   - Click "Create Database"
   - Choose region closest to your users (e.g., `ap-southeast-2` for Australia)
   - Select "Free Tier" or "Pay-as-you-go"
   - Copy `UPSTASH_REDIS_REST_URL` and `UPSTASH_REDIS_REST_TOKEN`

2. **Install Dependencies:**
   ```bash
   npm install @upstash/ratelimit @upstash/redis
   ```

3. **Environment Variables:**
   Add to `.env.local`:
   ```
   UPSTASH_REDIS_REST_URL="your_redis_url"
   UPSTASH_REDIS_REST_TOKEN="your_redis_token"
   ```

### Sliding Window Rate Limiting with Redis

Upstash provides three algorithms:
1. **Fixed Window**: Simple, resets at fixed intervals
2. **Sliding Window**: Smoother, counts requests in moving window
3. **Token Bucket**: Allows bursts while maintaining average rate

**Recommended: Sliding Window** - Prevents users from making 100 requests at 00:00 and another 100 at 00:01.

### Example Code for Next.js API Routes

```typescript
// lib/rate-limit-redis.ts
import { Ratelimit } from "@upstash/ratelimit";
import { Redis } from "@upstash/redis";

// Initialize Redis client
const redis = Redis.fromEnv();

// Create rate limiter with sliding window
// 100 requests per 15 minutes (matching current config)
export const ratelimit = new Ratelimit({
  redis,
  limiter: Ratelimit.slidingWindow(100, "15 m"),
  analytics: true, // Optional: track analytics
  prefix: "@upstash/ratelimit", // Optional: key prefix
});

// Helper to get client IP (compatible with existing code)
export function getClientIP(request: Request): string {
  const forwarded = request.headers.get('x-forwarded-for');
  if (forwarded) {
    return forwarded.split(',')[0].trim();
  }
  
  const realIP = request.headers.get('x-real-ip');
  if (realIP) return realIP;
  
  return 'unknown';
}

// Main rate limit check function
export async function checkRateLimit(
  identifier: string,
  maxRequests?: number,
  windowMs?: number
): Promise<{
  allowed: boolean;
  remaining: number;
  resetTime: number;
  totalLimit: number;
}> {
  // Convert windowMs to string format for Upstash
  const window = windowMs ?? 15 * 60 * 1000; // 15 minutes default
  const limit = maxRequests ?? 100; // 100 requests default
  
  // Convert ms to human-readable format
  const windowStr = msToHuman(window);
  
  // Create dynamic ratelimiter
  const dynamicRatelimit = new Ratelimit({
    redis,
    limiter: Ratelimit.slidingWindow(limit, windowStr),
  });
  
  const result = await dynamicRatelimit.limit(identifier);
  
  return {
    allowed: result.success,
    remaining: result.remaining,
    resetTime: result.reset, // Already in milliseconds
    totalLimit: limit,
  };
}

// Helper to convert milliseconds to human-readable format
function msToHuman(ms: number): string {
  const seconds = Math.floor(ms / 1000);
  const minutes = Math.floor(seconds / 60);
  const hours = Math.floor(minutes / 60);
  const days = Math.floor(hours / 24);
  
  if (days > 0) return `${days} d`;
  if (hours > 0) return `${hours} h`;
  if (minutes > 0) return `${minutes} m`;
  return `${seconds} s`;
}

// Get rate limit headers
export async function getRateLimitHeaders(
  identifier: string,
  maxRequests?: number,
  windowMs?: number
): Promise<Record<string, string>> {
  const limit = await checkRateLimit(identifier, maxRequests, windowMs);
  return {
    'X-RateLimit-Limit': limit.totalLimit.toString(),
    'X-RateLimit-Remaining': Math.max(0, limit.remaining).toString(),
    'X-RateLimit-Reset': new Date(limit.resetTime).toISOString(),
  };
}

// Create rate limit response
export function createRateLimitResponse(
  identifier: string,
  retryAfterSeconds?: number
): Response {
  const headers = new Headers({
    'Content-Type': 'application/json',
    'X-RateLimit-Limit': '100',
    'X-RateLimit-Remaining': '0',
  });
  
  if (retryAfterSeconds) {
    headers.set('Retry-After', retryAfterSeconds.toString());
  }
  
  return new Response(
    JSON.stringify({
      error: 'Rate limit exceeded. Please try again later.',
      retryAfter: retryAfterSeconds,
    }),
    {
      status: 429,
      headers,
    }
  );
}

// Export constants for backward compatibility
export const MAX_REQUESTS = 100;
export const WINDOW_MS = 15 * 60 * 1000; // 15 minutes
```

**Usage in API Route:**
```typescript
// app/api/route.ts
import { ratelimit, getClientIP, createRateLimitResponse } from '@/lib/rate-limit-redis';

export async function POST(request: Request) {
  const identifier = getClientIP(request);
  const result = await ratelimit.limit(identifier);
  
  if (!result.success) {
    return createRateLimitResponse(identifier, 60); // Retry after 60 seconds
  }
  
  // Process request...
  return Response.json({ message: 'Success' });
}
```

## 2. Alternative Solutions

### Vercel KV (Deprecated/Transitioned)
- **Status**: Vercel KV has been sunset (May 2023)
- **Alternative**: Vercel now recommends using marketplace storage solutions
- **Note**: Vercel KV was essentially Upstash Redis under the hood
- **Recommendation**: Use Upstash directly for better control and pricing

### Cloudflare Workers Rate Limiting
- **KV Storage**: $0.50/GB-month, 1GB free on Workers Paid plan
- **Rate Limiting**: Built into Workers platform
- **Pros**: Excellent performance, global edge network
- **Cons**: Requires Cloudflare Workers deployment, not ideal for Vercel-hosted Next.js
- **Best for**: Apps already on Cloudflare Workers platform

### Arcjet (Serverless-First)
- **Pricing**: Free tier available, paid plans start at $20/month
- **Pros**: No Redis infrastructure needed, handles rate limiting in cloud
- **Cons**: Third-party dependency, potential latency for edge decisions
- **Best for**: Teams wanting zero infrastructure management

### Other Options:
1. **Redis Labs (Redis Cloud)**: Similar to Upstash, more enterprise-focused
2. **AWS ElastiCache**: More expensive, better for large-scale apps
3. **PlanetScale + KV Store**: If already using PlanetScale for database

## 3. Implementation Guide

### Step-by-Step Setup for Upstash

**Step 1: Create Upstash Account**
1. Go to [upstash.com](https://upstash.com) and sign up
2. Verify email address

**Step 2: Create Redis Database**
1. In dashboard, click "Create Database"
2. Name: `rate-right-rate-limiting`
3. Region: `ap-southeast-2` (Sydney) for Australian users
4. Plan: Start with "Free Tier"
5. Click "Create"

**Step 3: Get Credentials**
1. Click on your new database
2. Copy `UPSTASH_REDIS_REST_URL`
3. Copy `UPSTASH_REDIS_REST_TOKEN`

**Step 4: Update Environment**
```bash
# .env.local
UPSTASH_REDIS_REST_URL="your_url_here"
UPSTASH_REDIS_REST_TOKEN="your_token_here"
```

**Step 5: Install Dependencies**
```bash
cd /home/ccuser/the-50-dollar-app
npm install @upstash/ratelimit @upstash/redis
```

### Code Migration: Convert `lib/rate-limit.ts`

**Option A: Complete Replacement (Recommended)**
1. Create new file: `lib/rate-limit-redis.ts` (code above)
2. Update all imports from `@/lib/rate-limit` to `@/lib/rate-limit-redis`
3. Remove old `lib/rate-limit.ts` or keep as backup

**Option B: Hybrid Approach (Backward Compatible)**
```typescript
// lib/rate-limit.ts - Updated version
import { Ratelimit } from "@upstash/ratelimit";
import { Redis } from "@upstash/redis";

// Initialize Redis if environment variables exist
let redis: Redis | null = null;
let ratelimit: Ratelimit | null = null;

try {
  if (process.env.UPSTASH_REDIS_REST_URL && process.env.UPSTASH_REDIS_REST_TOKEN) {
    redis = Redis.fromEnv();
    ratelimit = new Ratelimit({
      redis,
      limiter: Ratelimit.slidingWindow(100, "15 m"),
      analytics: true,
    });
    console.log('Upstash Redis rate limiting enabled');
  } else {
    console.warn('UPSTASH_REDIS_REST_URL or UPSTASH_REDIS_REST_TOKEN not set. Using in-memory rate limiting.');
  }
} catch (error) {
  console.error('Failed to initialize Upstash Redis:', error);
}

// Fallback to in-memory store if Redis not available
const rateLimitStore = new Map<string, { count: number; resetTime: number }>();
const WINDOW_MS = 15 * 60 * 1000;
const MAX_REQUESTS = 100;

export async function checkRateLimit(
  identifier: string,
  maxRequests?: number,
  windowMs?: number
): Promise<{
  allowed: boolean;
  remaining: number;
  resetTime: number;
  totalLimit: number;
}> {
  // Use Redis if available
  if (ratelimit && redis) {
    const limit = maxRequests ?? MAX_REQUESTS;
    const window = windowMs ?? WINDOW_MS;
    const windowStr = msToHuman(window);
    
    const dynamicRatelimit = new Ratelimit({
      redis,
      limiter: Ratelimit.slidingWindow(limit, windowStr),
    });
    
    const result = await dynamicRatelimit.limit(identifier);
    
    return {
      allowed: result.success,
      remaining: result.remaining,
      resetTime: result.reset,
      totalLimit: limit,
    };
  }
  
  // Fallback to in-memory implementation
  // ... existing in-memory code ...
}

// Keep other functions with async modifications
```

### Environment Variables Needed

**Required:**
```
UPSTASH_REDIS_REST_URL="https://global-xxx.upstash.io"
UPSTASH_REDIS_REST_TOKEN="your_token_here"
```

**Optional (for testing):**
```
RATE_LIMIT_DISABLED="false"  # Set to "true" to disable rate limiting in dev
RATE_LIMIT_MAX_REQUESTS="100" # Override default
RATE_LIMIT_WINDOW_MS="900000" # Override default (15 minutes)
```

### Testing Approach

**1. Unit Tests:**
```typescript
// __tests__/rate-limit-redis.test.ts
import { checkRateLimit, getClientIP } from '@/lib/rate-limit-redis';

describe('Rate Limiting', () => {
  it('should allow requests within limit', async () => {
    const result = await checkRateLimit('test-ip-1', 5, 60000);
    expect(result.allowed).toBe(true);
    expect(result.remaining).toBe(4);
  });
  
  it('should block requests over limit', async () => {
    const identifier = 'test-ip-2';
    
    // Make 5 requests
    for (let i = 0; i < 5; i++) {
      await checkRateLimit(identifier, 5, 60000);
    }
    
    // 6th should be blocked
    const result = await checkRateLimit(identifier, 5, 60000);
    expect(result.allowed).toBe(false);
    expect(result.remaining).toBe(0);
  });
});
```

**2. Integration Tests:**
- Test with actual Redis instance (use Upstash free tier)
- Test IP extraction from various headers
- Test concurrent requests from same IP

**3. Load Testing:**
```bash
# Using k6 or artillery
artillery quick --count 1000 --num 10 http://localhost:3000/api/test
```

**4. Monitoring:**
- Set up Upstash dashboard alerts
- Monitor command usage
- Track rate limit hits in application logs

## 4. Migration Strategy

### Phase 1: Setup & Testing (1-2 days)
1. Create Upstash Redis database
2. Implement new rate limiting module
3. Write comprehensive tests
4. Test in staging environment

### Phase 2: Gradual Rollout (1 day)
1. Deploy to production with feature flag
2. Monitor Redis usage and performance
3. Compare with old in-memory implementation
4. Fix any issues

### Phase 3: Full Migration (1 day)
1. Remove old in-memory implementation
2. Update all API routes to use new module
3. Remove feature flag
4. Update documentation

### Phase 4: Optimization & Monitoring (Ongoing)
1. Set up alerts for rate limit breaches
2. Monitor Redis costs
3. Adjust limits based on usage patterns
4. Consider multi-region setup if global traffic grows

## 5. Cost Estimation

**Free Tier (Initial):**
- 500K commands/month = ~166K rate limit checks
- 256MB storage = More than enough
- $0/month

**Growth Scenario (10K users):**
- Assuming 100 requests/user/day = 1M requests/day
- Rate limit checks: ~3M commands/day = 90M/month
- Cost: 90M commands = 900 * $0.20 = $180/month
- Still well within affordable range for production app

## 6. Security Considerations

1. **Token Security**: Keep `UPSTASH_REDIS_REST_TOKEN` in environment variables, never in code
2. **IP Spoofing**: Be aware that `X-Forwarded-For` can be spoofed
3. **DDoS Protection**: Rate limiting helps but consider additional DDoS protection
4. **Key Namespacing**: Use prefixes to avoid collisions if sharing Redis instance
5. **Data Persistence**: Rate limit data is ephemeral (keys expire), no PII stored

## 7. Performance Considerations

1. **Latency**: Upstash Redis REST API adds ~10-50ms latency per check
2. **Caching**: Upstash SDK includes local caching for "hot" paths
3. **Connection Pooling**: SDK handles connection management
4. **Region Selection**: Choose region closest to your Vercel deployment

## Conclusion

**Upstash Redis is the clear winner** for the $50 app:
- ✅ Free tier sufficient for launch
- ✅ Seamless Vercel/Next.js integration
- ✅ Production-ready with scaling options
- ✅ No infrastructure management
- ✅ Excellent documentation and community support

**Next Steps:**
1. Create Upstash account and Redis database
2. Implement `lib/rate-limit-redis.ts`
3. Update environment variables
4. Test thoroughly in staging
5. Deploy to production with monitoring

The migration will ensure rate limiting works correctly across serverless invocations and scales with the application's growth.