STRIPE STARTER · RATE LIMITING (UPSTASH)
Rate limiting (Upstash)
Sliding-window rate limiting on createCheckoutSession (10/60s) and createPortalSession (20/60s), per IP. Upstash Redis over REST — serverless-friendly. Graceful no-op when not configured.
Server Actions that create Stripe sessions are abusable: a bot can call them in a loop and create thousands of Checkout sessions per minute. Stripe will accept them all (they're free until paid). Rate limiting puts a brake on it — and it's per-IP, so one bad actor doesn't lock out everyone.
The two limits
- Checkout: 10 requests / 60 seconds per IP (sliding window).
- Portal: 20 requests / 60 seconds per IP (sliding window) — more lenient since reaching the portal already means the user is authenticated.
Turn it on
# Upstash console → Create database → "REST API"
UPSTASH_REDIS_REST_URL="https://xxx.upstash.io"
UPSTASH_REDIS_REST_TOKEN="AXxx..."Graceful degradation when not configured
When the Upstash env vars are absent, the limiter constructors return null. checkRateLimit(null, id) returns { allowed: true } — every request passes. This means dev works without Upstash and ALL callers don't need to check "is Upstash configured?" themselves.
// src/lib/rate-limit.ts
export const checkoutRateLimiter = redis
? new Ratelimit({
redis,
limiter: Ratelimit.slidingWindow(10, "60 s"),
prefix: "ratelimit:checkout",
})
: null;
// In the Server Action:
const result = await checkRateLimit(checkoutRateLimiter, ip);
if (!result.allowed) {
return { success: false, error: `Try again in ${result.retryAfter}s.` };
}IP extraction — proxy headers, in order
// src/actions/checkout.ts → getClientIp()
x-forwarded-for → first IP in the comma-separated list (the client)
x-real-ip → fallback for some Nginx setups
"unknown" → final fallback (all unknown IPs share one bucket — fine for dev)Trust your proxy or rate limiting lies
x-forwarded-for is set by your reverse proxy (Vercel, Cloudflare, your own Nginx). If your platform doesn't sit behind a trusted proxy, an attacker can spoof the header — and bypass per-IP limits trivially. On Vercel and Cloudflare you're fine by default. On a raw VPS, configure your proxy to overwrite the header.
Sliding window vs fixed window
Sliding window means the 60-second budget rolls continuously. You can't burst 10 at second 59 and another 10 at second 61. We pick it because checkout abuse is bursty — fixed windows leave a half-second gap attackers exploit.
Tune the limits
10/60s on checkout is intentionally tight — real users click "buy" a handful of times max. If your funnel has higher legitimate retry pressure (e.g., multi-product checkout pages), bump it. Edit src/lib/rate-limit.ts and restart.