pealdevDOCS
Back to peal.dev

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

Terminal
# 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.

TypeScript
// 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

TypeScript
// 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.