The Rate Limiting Gap
You built an app with Cursor, deployed it, and within a week, your database was overwhelmed by a bot scraping your API, your login endpoint was brute-forced, and your password reset endpoint was abused to send thousands of emails. What happened? Your app worked, so why was it so easy to abuse? This is the rate limiting gap, and it is one of the most common security failures in AI-generated apps. AI assistants rarely add rate limiting, because it is not needed for the app to function and is easy to overlook. The result is apps that are vulnerable to brute force, scraping, and DDoS attacks. Here is the production checklist to fix it.
The direct answer is that rate limiting is a mechanism that limits the number of requests a user (or IP address) can make in a given time period, which protects your app from abuse. AI assistants rarely add rate limiting, because it is not needed for the app to function and is not part of the default boilerplate. The result is apps where any user (or bot) can make unlimited requests, which leads to brute force attacks (on login), scraping (on public APIs), and DDoS attacks (on any endpoint). The fix is to add rate limiting to all public endpoints, with different limits for different endpoint types. For more on why AI apps fail under real conditions, see our article on why AI apps break on the first real user.
Why AI Assistants Skip Rate Limiting
Three reasons explain why AI assistants skip rate limiting. First, it is not needed for the app to function. Rate limiting is a production concern, not a functionality concern, so the LLM does not include it in the default code. Second, it is not part of the default boilerplate. Most framework starters (Next.js, Vite, Express) do not include rate limiting by default, so the LLM does not see it in the training data's boilerplate. Third, it is complex to implement correctly. Rate limiting requires a store (to track request counts), a strategy (per IP, per user, per endpoint), and limits (requests per minute, per hour, per day). The LLM often skips this complexity, because it is not sure how to implement it correctly. The result is apps that are wide open to abuse.
The 5 Endpoints That Need Rate Limiting
Not all endpoints need the same rate limiting. Here are the 5 types of endpoints that need rate limiting, with recommended limits for each.
1. Login endpoint
The login endpoint is the most common target for brute force attacks. An attacker tries thousands of password combinations, hoping to guess the correct one. Without rate limiting, they can try thousands per second. The fix is to limit each IP to 5 login attempts per minute, and to lock the account (or require a CAPTCHA) after 10 failed attempts. For Express, use express-rate-limit. For FastAPI, use slowapi. For more on authentication security, see our article on the JWT authentication trap.
2. Password reset endpoint
The password reset endpoint is often abused to send thousands of password reset emails, which costs money (email providers charge per email) and annoys users. The fix is to limit each IP to 3 password reset requests per hour, and to limit each email address to 3 requests per day. This prevents both spam and abuse.
3. Signup endpoint
The signup endpoint can be abused to create thousands of fake accounts, which pollutes your database and can be used for spam. The fix is to limit each IP to 5 signups per hour, and to require email verification before the account is active. This prevents mass account creation.
4. Public API endpoints
Public API endpoints (e.g., a search endpoint, a data endpoint) can be scraped by bots, which consumes your server resources and can expose your data. The fix is to limit each IP (or API key) to 100 requests per minute for public endpoints, and to require authentication for higher limits. This allows legitimate users while preventing scraping.
5. Expensive endpoints
Expensive endpoints (e.g., an AI generation endpoint, a report generation endpoint) can be abused to consume your server resources, which can lead to denial of service. The fix is to limit each user to 10 expensive requests per hour, and to queue requests for asynchronous processing if the limit is exceeded. This prevents resource exhaustion.
Step-by-Step: Implementing Rate Limiting in Express
Here is the exact workflow for implementing rate limiting in a typical Express app.
Step 1: Install express-rate-limit
npm install express-rate-limitStep 2: Configure rate limiters for different endpoint types
const rateLimit = require('express-rate-limit');
// Login rate limiter: 5 attempts per minute per IP
const loginLimiter = rateLimit({
windowMs: 60 * 1000,
max: 5,
message: { error: 'Too many login attempts. Please try again in a minute.' },
standardHeaders: true,
legacyHeaders: false,
});
// Password reset rate limiter: 3 requests per hour per IP
const passwordResetLimiter = rateLimit({
windowMs: 60 * 60 * 1000,
max: 3,
message: { error: 'Too many password reset requests. Please try again in an hour.' },
});
// Signup rate limiter: 5 signups per hour per IP
const signupLimiter = rateLimit({
windowMs: 60 * 60 * 1000,
max: 5,
message: { error: 'Too many signup attempts. Please try again in an hour.' },
});
// General API rate limiter: 100 requests per minute per IP
const apiLimiter = rateLimit({
windowMs: 60 * 1000,
max: 100,
message: { error: 'Too many requests. Please slow down.' },
});
// Expensive endpoint rate limiter: 10 requests per hour per user
const expensiveLimiter = rateLimit({
windowMs: 60 * 60 * 1000,
max: 10,
keyGenerator: (req) => req.user?.id || req.ip,
message: { error: 'You have reached your hourly limit for this action.' },
});Step 3: Apply the rate limiters to endpoints
app.post('/login', loginLimiter, loginHandler);
app.post('/password-reset', passwordResetLimiter, passwordResetHandler);
app.post('/signup', signupLimiter, signupHandler);
app.use('/api', apiLimiter); // apply to all /api routes
app.post('/api/generate', expensiveLimiter, generateHandler);Step 4: Use a Redis store for distributed rate limiting
If you run multiple containers, the default in-memory store does not work, because each container has its own count. The fix is to use a Redis store, which shares the count across containers.
npm install rate-limit-redis ioredisconst RedisStore = require('rate-limit-redis');
const IORedis = require('ioredis');
const redisClient = new IORedis(process.env.REDIS_URL);
const loginLimiter = rateLimit({
store: new RedisStore({
sendCommand: (...args) => redisClient.call(...args),
}),
windowMs: 60 * 1000,
max: 5,
});Step 5: Set environment variables
In the Deployxa dashboard, set REDIS_URL to your Redis connection string. The pre-flight scanner will warn you if it is missing. For more on Redis setup, see our article on running BullMQ background workers on persistent containers.
Step 6: Verify with deployxa doctor
Run deployxa doctor to verify your app's health. The 14-point readiness engine checks SSL, DNS, environment variables, health endpoints, and container status.
Common Pitfalls and Troubleshooting
The first pitfall is using the default in-memory store in a multi-container setup. The in-memory store is per-container, which means each container has its own count, and the effective limit is multiplied by the number of containers. The fix is to use a Redis store for distributed rate limiting. The second pitfall is setting limits too high. If the limit is too high, it does not prevent abuse. The fix is to start with conservative limits and to adjust based on monitoring. The third pitfall is setting limits too low. If the limit is too low, legitimate users are blocked. The fix is to monitor the rate limit headers (e.g., X-RateLimit-Remaining) and to adjust based on user feedback. The fourth pitfall is not handling rate limit errors gracefully. When a user is rate limited, the API should return a 429 status code with a clear error message and a Retry-After header. The fix is to configure the rate limiter to return a 429 status code and to include the Retry-After header. The fifth pitfall is not rate limiting authenticated endpoints. Some developers assume that authenticated endpoints do not need rate limiting, but authenticated users can also abuse the API (e.g., a compromised account). The fix is to rate limit all endpoints, with higher limits for authenticated users.
The Production Checklist: Beyond Rate Limiting
Rate limiting is one piece of the production security puzzle. A fully production-ready app also needs authentication, authorization, input validation, output encoding, CSRF protection, and security headers. For more on security, see our articles on the JWT authentication trap and securing agentic cloud deployments. For more on production readiness, see our articles on the 5 common AI coding mistakes and why AI apps break on the first real user.
Production Hardening for Rate Limiting
Beyond the 5 endpoints, rate limiting systems benefit from several additional hardening steps. The first is distributed rate limiting. If you run multiple containers, the rate limiter needs to use a shared store (e.g., Redis) to count requests across all containers. Without a shared store, each container has its own count, and the effective limit is multiplied by the number of containers. The second is adaptive rate limiting. Instead of fixed limits, the rate limiter can adapt based on the server's load: when the server is lightly loaded, allow higher limits; when heavily loaded, lower the limits. This maximizes utilization without overloading the server. The third is IP rotation detection. Sophisticated attackers use IP rotation (e.g., botnets) to bypass per-IP rate limits. The fix is to also rate limit by user account (for authenticated endpoints) or by fingerprint (e.g., browser fingerprint, TLS fingerprint). The fourth is CAPTCHA integration. When a user exceeds the rate limit, instead of returning a 429 error, the rate limiter can require a CAPTCHA to continue. This deters bots while allowing legitimate users to proceed. The fifth is rate limit headers. Include X-RateLimit-Limit, X-RateLimit-Remaining, and X-RateLimit-Reset headers in responses, so clients can adjust their request rate to avoid hitting the limit. For more on security, see our articles on the JWT authentication trap and securing agentic cloud deployments.
When Rate Limiting Is Not Needed
Not every endpoint needs rate limiting. Internal endpoints (e.g., admin dashboards) that are only accessed by trusted users do not need rate limiting. Endpoints that are already protected by other mechanisms (e.g., a CDN that rate limits at the edge) might not need additional rate limiting at the app level. For these endpoints, adding rate limiting adds complexity without providing significant benefit. The key is to focus rate limiting on public, unauthenticated endpoints where abuse is most likely. For more on production patterns, see our articles on why AI apps break on the first real user and the file upload trap.
Conclusion: Rate Limiting Is Not Optional
The rate limiting gap is not a sign that your AI assistant did a bad job. It is a sign that rate limiting is a production concern that AI assistants skip, because it is not needed for the app to function. By applying the production checklist above (rate limit login, password reset, signup, public API, and expensive endpoints), you can protect your app from abuse and ensure it remains available for legitimate users. Stop shipping unprotected apps and start rate limiting.
Ready to deploy a rate-limited app? Drag your project to Deployxa Drop for an instant live preview, or install the CLI with npm i -g @deployxa/cli and deploy from your terminal. For more on AI coding patterns, see our articles on AI error handling failures and the file upload trap. Learn about why AI apps have no SEO and why AI apps break on mobile in our companion articles. Explore our free developer tools to speed up your workflow.