Launch checklist implementation: - Add Privacy Policy page (/privacy) with GDPR compliance - Add Terms of Service page (/terms) - Add custom 404 and error pages with helpful navigation - Add FAQ page (/faq) covering common questions - Add SEO meta tags with OpenGraph/Twitter cards - Add JSON-LD structured data (Organization, WebSite, SoftwareApplication) - Add sitemap.ts and robots.ts for search engines - Add security headers (HSTS, CSP, X-Frame-Options) in vercel.json - Add security.txt at /.well-known/security.txt - Add API rate limiting (100 req/min default, 20 req/min strict) - Add empty states in tool search for better UX - Update AppHeader with FAQ link - Update AppFooter with Privacy/Terms links - Update biome.json to allow dangerouslySetInnerHTML for JSON-LD in page files 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
9.3 KiB
API Rate Limiting
This document describes the rate limiting implementation for TPMJS.com's public API endpoints.
Overview
Rate limiting has been implemented to protect the API from abuse and ensure fair usage across all clients. The implementation uses an in-memory sliding window algorithm suitable for Vercel's serverless architecture.
Configuration
Rate Limit Tiers
Two rate limit configurations are available:
Default Rate Limit (100 requests/minute):
import { checkRateLimit } from '~/lib/rate-limit';
export async function GET(request: NextRequest) {
const rateLimitResponse = checkRateLimit(request);
if (rateLimitResponse) {
return rateLimitResponse;
}
// ... rest of handler
}
Strict Rate Limit (20 requests/minute):
import { checkRateLimit, STRICT_RATE_LIMIT } from '~/lib/rate-limit';
export async function GET(request: NextRequest) {
const rateLimitResponse = checkRateLimit(request, STRICT_RATE_LIMIT);
if (rateLimitResponse) {
return rateLimitResponse;
}
// ... rest of handler
}
Custom Rate Limits
You can define custom rate limits:
import { checkRateLimit, type RateLimitConfig } from '~/lib/rate-limit';
const customLimit: RateLimitConfig = {
limit: 50, // 50 requests
windowSeconds: 60, // per minute
};
const rateLimitResponse = checkRateLimit(request, customLimit);
Protected Endpoints
Default Rate Limit (100 req/min)
GET /api/health- Health check endpointGET /api/tools- List and filter toolsGET /api/tools/[...slug]- Get specific tool/package detailsPOST /api/tools/[...slug]- Trigger manual health checkGET /api/tools/broken- List broken toolsGET /api/stats- Get registry statisticsPOST /api/tools/validate- Validate tpmjs fieldPOST /api/tools/report-health- Report tool execution results
Strict Rate Limit (20 req/min)
GET /api/tools/search- Expensive BM25 search operations
Exempt from Rate Limiting
Cron jobs are automatically exempt when authenticated with CRON_SECRET:
POST /api/sync/changes- NPM changes feed syncPOST /api/sync/keyword- NPM keyword search syncPOST /api/sync/metrics- Download stats and quality score updates
How It Works
IP-Based Tracking
Rate limits are tracked per client IP address using the following header priority:
x-forwarded-for(set by Vercel)x-real-ip- Fallback to 'unknown' if neither is available
Sliding Window Algorithm
The implementation uses a sliding window counter:
- Each request timestamp is stored in memory
- On each request, timestamps older than the window are removed
- If remaining timestamps exceed the limit, request is rejected
- Otherwise, current timestamp is added and request proceeds
Memory Management
To prevent memory leaks in long-running serverless instances:
- Automatic cleanup runs every 5 minutes
- Entries older than 1 minute are removed
- Store size is capped at 10,000 entries
- If cap is exceeded, oldest 20% of entries are removed
Cron Job Bypass
Requests with valid Authorization: Bearer <CRON_SECRET> header bypass rate limiting entirely. This is checked at the start of checkRateLimit():
const authHeader = request.headers.get('authorization');
const token = authHeader?.replace('Bearer ', '');
if (env.CRON_SECRET && token === env.CRON_SECRET) {
return null; // Allow cron jobs to bypass
}
Response Format
When rate limited, endpoints return HTTP 429 with the following format:
{
"success": false,
"error": "Rate limit exceeded",
"message": "Too many requests. Please try again in 45 seconds.",
"retryAfter": 45,
"limit": 100,
"window": 60
}
Response Headers
Rate-limited responses include these headers:
Retry-After: Seconds until rate limit resetsX-RateLimit-Limit: Maximum requests allowed in windowX-RateLimit-Remaining: Requests remaining (always 0 when rate limited)X-RateLimit-Reset: Unix timestamp when rate limit resets
Testing Rate Limits
Manual Testing
Test rate limiting with curl:
# Test basic rate limit
for i in {1..105}; do
curl -i http://localhost:3000/api/health
sleep 0.1
done
# Should see 429 responses after request 100
Check Rate Limit Status
For debugging, you can check rate limit status (not exposed as public API):
import { getRateLimitStatus } from '~/lib/rate-limit';
const status = getRateLimitStatus(request);
console.log({
clientId: status.clientId,
used: status.used,
remaining: status.remaining,
resetAt: status.resetAt,
});
Limitations
Serverless Architecture Considerations
-
Per-Instance State: Rate limits are tracked per serverless instance, not globally
- Multiple concurrent instances don't share state
- Under high load, effective rate limit may be higher than configured
- For production-grade global rate limiting, consider Upstash Redis or Vercel KV
-
Cold Starts: Rate limit state is lost when serverless instance scales down
- This is acceptable for moderate traffic
- Persistent storage would be needed for strict enforcement
-
IP Spoofing: Relies on headers set by Vercel
- Headers cannot be spoofed by clients (Vercel sets them)
- But multiple users behind same NAT share one IP and one rate limit
Production Recommendations
For high-traffic production use, consider upgrading to a distributed solution:
Option 1: Upstash Redis
import { Ratelimit } from '@upstash/ratelimit';
import { Redis } from '@upstash/redis';
const ratelimit = new Ratelimit({
redis: Redis.fromEnv(),
limiter: Ratelimit.slidingWindow(100, '1 m'),
});
export async function GET(request: NextRequest) {
const ip = request.headers.get('x-forwarded-for') ?? 'unknown';
const { success } = await ratelimit.limit(ip);
if (!success) {
return NextResponse.json(
{ error: 'Rate limit exceeded' },
{ status: 429 }
);
}
// ... rest of handler
}
Option 2: Vercel KV
import { kv } from '@vercel/kv';
async function checkRateLimit(ip: string) {
const key = `ratelimit:${ip}`;
const count = await kv.incr(key);
if (count === 1) {
await kv.expire(key, 60); // 1 minute window
}
return count <= 100;
}
Monitoring
Vercel Logs
Rate limit events are logged automatically. Monitor with:
vercel logs <deployment-url> | grep "Rate limit"
Analytics
Consider adding analytics to track:
- Rate limit hit rate
- Most rate-limited IPs
- Rate limit effectiveness
Example implementation:
if (entry.timestamps.length >= config.limit) {
console.log(`[Rate Limit] Blocked request from ${clientId} (${entry.timestamps.length}/${config.limit})`);
// Optional: Send to analytics
// await analytics.track('rate_limit_exceeded', {
// ip: clientId,
// endpoint: request.url,
// limit: config.limit,
// });
}
Future Improvements
Potential enhancements:
- Add rate limit status endpoint for debugging (
GET /api/rate-limit/status) - Implement user-based rate limiting (for authenticated requests)
- Add configurable rate limits per endpoint via environment variables
- Implement exponential backoff suggestions in error messages
- Add Prometheus metrics for rate limit monitoring
- Support for allowlisting trusted IPs
- Implement burst allowance (e.g., allow 120 req/min with 100 sustained)
Troubleshooting
"Rate limit exceeded" for legitimate traffic
If users report false positives:
- Check if multiple users are behind same NAT/proxy
- Consider increasing limits for that endpoint
- Implement user authentication to enable per-user limits
- Use distributed rate limiting (Upstash/KV) for accurate counts
Rate limiting not working
- Verify
checkRateLimit()is called before request processing - Check that Vercel is setting
x-forwarded-forheader - Ensure serverless instances are staying warm (check Vercel logs)
- Test with unique IP addresses (use different VPN endpoints)
Cron jobs being rate limited
- Verify
CRON_SECRETis set in Vercel environment variables - Check that cron requests include
Authorization: Bearer <CRON_SECRET>header - Vercel Cron automatically adds this header - manual testing requires adding it
# Test cron endpoint with auth
curl -X POST https://tpmjs.com/api/sync/changes \
-H "Authorization: Bearer $CRON_SECRET"
Security Considerations
-
DDoS Protection: This rate limiter provides basic protection but is not a substitute for proper DDoS mitigation (use Cloudflare, Vercel's built-in protection, etc.)
-
IP Spoofing: Cannot spoof
x-forwarded-forheader - Vercel controls it -
Bypass Attempts: Rate limit is enforced server-side, cannot be bypassed by clients
-
CRON_SECRET: Keep this secret secure - anyone with it can bypass rate limits
Related Files
/apps/web/src/lib/rate-limit.ts- Core rate limiting implementation/apps/web/src/env.ts- Environment configuration (CRON_SECRET)/apps/web/src/app/api/**/*.ts- Protected API endpoints