Common endpoints to rate-limit: login, signup, password reset, public search, contact form, SMS sending, any expensive AI or external-API call.
Three Paths
- In-memory counter
- Database-backed
- Redis / Upstash (recommended)
Fastest to write, zero dependencies. Only works per-container — state is lost on cold start or if you have multiple instances. Fine for quick protection on low-traffic sites; not sufficient at scale.
Redis Pattern with Upstash
Sign up at Upstash
Go to upstash.com, create a Redis database. The free tier handles 10k commands/day — plenty for moderate-traffic sites.
Add env vars in Hiveku
Settings > Environment Variables. Add both. See Environment Variables.
Common Limits by Endpoint
| Endpoint | Suggested limit |
|---|---|
| Login | 5 attempts per minute per IP |
| Signup | 3 per minute per IP |
| Password reset | 3 per hour per email |
| Public search | 30 per minute per IP |
| Contact form | 5 per hour per IP |
| Send SMS | 10 per day per phone number |
| AI chat / expensive API | 100 per day per user |
Choosing the Rate-Limit Key
What you limit by matters as much as the number:- By IP. Simple. Blocks coffee shops and corporate NATs if abused (false positives). Good for pre-auth endpoints where you have no better identifier.
- By user ID. Targeted. Doesn’t help for pre-auth endpoints like login or signup.
- By email or phone number. Good for signup/login/reset — stops account-creation farming where an attacker rotates IPs but reuses the same email pattern.
- Composite keys. For login: limit by IP AND by email. An attacker hitting one email from many IPs triggers the email limit; one IP hitting many emails triggers the IP limit. Both must be low to get through.
Return Proper 429 Response Headers
Clients can behave correctly if you tell them the limit state:Retry-After is the most important — well-behaved clients will back off for that many seconds.
Client-Side Handling
When you get a 429:Show a friendly error
Not “Error 429”. Something like “You’re doing that too fast. Try again in 30 seconds.”
Show a countdown
Display a countdown timer based on
Retry-After. Disable the submit button until it elapses.Database-Backed Pattern
If Redis isn’t an option, a minimal DB approach:DELETE FROM rate_limits WHERE window_start < now() - interval '1 hour'.
What NOT to Rate-Limit
Good candidates: per-IP + per-user composite limits that are high enough never to hit in normal usage but low enough to block scripts.Verify It Worked
Confirm 429 after the limit
The first N requests should succeed (or return the usual login error). Request N+1 should return
429 Too Many Requests.Troubleshooting
Limits aren't enforcing at all
Limits aren't enforcing at all
Check env vars. If
UPSTASH_REDIS_REST_URL or UPSTASH_REDIS_REST_TOKEN is missing, the SDK silently fails closed or open depending on config. Log the result of ratelimit.limit(...) to confirm it’s actually being hit.Legitimate users getting blocked
Legitimate users getting blocked
Your limits are too aggressive. Check your analytics for typical request rates from real users and set limits comfortably above the 99th percentile. Also consider switching from by-IP to by-user ID for authenticated endpoints.
Rate limit state lost on deploy
Rate limit state lost on deploy
You’re using in-memory limits. They don’t survive cold starts or new deployments. Switch to Redis or DB-backed limits.
Behind a load balancer, everyone shares an IP
Behind a load balancer, everyone shares an IP
CloudFront/Cloudflare also blocking
CloudFront/Cloudflare also blocking
Your WAF and app-level rate limits are double-counting. Either disable WAF rate limiting for these routes and handle it all at the app, or the opposite. Don’t stack — it confuses users and makes debugging hard.
What’s Next?
Environment Variables
Manage Redis and other service credentials safely
View Logs
Debug rate limiting issues in production