Practical Rate Limiting and Abuse Prevention for Indie SaaS
A practical guide for solo founders: what to rate-limit, how to implement it with minimal infrastructure, how to respond to clients, and complementary controls so abuse does not become an expensive surprise.
A single unauthenticated endpoint or a cheap AI feature can become expensive overnight. Credential-stuffing bots, scrapers, or a well-meaning customer who loops a request can burn through free-tier quotas, model credits, or database connections. Solo founders usually notice after the bill or the support ticket arrives.
Rate limiting is the baseline control that keeps usage bounded. It is not a complete security system, and it will not stop every determined attacker. It does stop the common, high-volume cases that turn a quiet product into an operational problem.
This is a practical operating guide, not a survey of every algorithm. The goal is a setup you can ship and maintain alone.
Decide what actually needs a limit
Not every route deserves the same treatment. Start with the surfaces that cost money, create accounts, or change state.
Typical high-value targets (illustrative; match them to your product):
- Authentication and account recovery (login, password reset, magic links, MFA). These are classic targets for credential stuffing and enumeration.
- Expensive operations (AI model calls, large exports, image/video generation, bulk queries, external API proxies).
- Signup and free-trial creation.
- Webhook receivers and public form endpoints.
- Any endpoint that can trigger email, SMS, or third-party side effects.
Read-heavy public pages or static assets can usually stay unlimited or use a very high ceiling. Over-limiting them creates support load without protecting cost.
Document the limits you choose (requests per minute, per hour, or per day) in your API docs or in-app help. Customers who hit them should not be surprised.
Choose the identity you count against
The key you rate-limit on determines both fairness and how easy the limit is to evade.
Common choices:
- Authenticated user or API key — preferred for logged-in traffic. One user cannot starve others.
- IP address — useful for unauthenticated endpoints. Shared NAT, mobile carriers, and corporate proxies mean many real users can share an IP; set higher ceilings or combine with other signals.
- User + IP or API key + IP — tighter control for sensitive routes.
- Global — a hard ceiling on total traffic to an expensive path so a bug or sudden spike cannot exhaust your entire budget.
For AI features, prefer a per-user or per-organization limit that is tighter than the infrastructure can theoretically handle. A single runaway script should not consume the month’s model budget.
Simple algorithms that are good enough
You do not need a perfect distributed rate limiter on day one.
- Fixed window — count requests in the current minute (or hour). Easy to implement. A burst at the boundary can briefly exceed the intended rate.
- Sliding window or token bucket — smoother behavior. Token bucket is especially practical: each identity has a bucket of tokens that refills at a steady rate; each request consumes one or more tokens.
For a single-process app, an in-memory map or a library that wraps it is often sufficient while traffic is low. When you run more than one instance, move the counter to Redis (or your database with care) so limits are shared. Many frameworks already have middleware that supports both.
If you sit behind Cloudflare, AWS API Gateway, or a similar edge, start with their built-in rate-limiting rules for the public surface and keep application-level limits for authenticated or expensive work. Edge limits are cheap to operate and protect you even when your app is down.
Respond clearly and safely
When a client exceeds the limit:
- Return HTTP 429 Too Many Requests.
- Include a
Retry-Afterheader (seconds) or a response body that states when they can try again. - Keep the body small and free of internal details.
- Log the event with the identity, route, and current count so you can see patterns later.
Do not return 401 or 500 for rate-limit hits; those statuses confuse clients and monitoring. Do not permanently ban an IP or user on the first 429. Legitimate users (or shared IPs) hit limits; give them a way to retry.
For user-facing products, surface a clear message in the UI instead of a raw JSON error. For APIs, document the exact headers and body shape so integrators can handle it.
Complementary controls that rate limits alone miss
Rate limits slow abuse; they do not eliminate it. Pair them with:
- Strong authentication and short-lived tokens where possible.
- Progressive friction on sensitive actions (CAPTCHA or proof-of-work after repeated failures, or simply a short cooldown).
- Input validation and size limits so a single request cannot be arbitrarily expensive.
- Spending caps or hard quotas on third-party services (model providers, email, SMS) that live outside your application rate limiter.
- Monitoring that alerts when rate-limit hits spike or when a single identity consumes a large fraction of a global budget.
Credential stuffing and scraping often combine many IPs. In those cases, rate limits per IP buy time while you add better signals (device fingerprinting is fragile; behavioral patterns and known-bad lists are more practical for a solo operator).
A practical checklist before you call it done
- Identify the 3–5 routes or actions that cost money or create accounts.
- Choose the identity (user, key, IP, or combination) and the numeric limit for each.
- Implement the check early in the request path (middleware or edge rule).
- Use a shared store (Redis or equivalent) if you run more than one instance.
- Return 429 +
Retry-After(or equivalent) and log the hit. - Document the limits for customers and integrators.
- Set an upstream spending or quota alert on expensive third-party services.
- Add a simple way to temporarily raise or bypass a limit for a known customer (support escape hatch).
- Verify that background jobs and webhooks are either excluded or limited appropriately so they do not starve themselves.
Common solo-founder pitfalls
- Applying a tight IP limit to an endpoint used by offices or mobile users, then spending the afternoon explaining why half the team is locked out.
- Rate-limiting only the happy path and forgetting the error or retry path, so a client in a loop still generates load.
- Counting only successful requests. Failed authentication attempts should usually count toward the limit.
- Storing rate-limit state in a way that disappears on restart or is not shared across instances, then wondering why the limit never triggers in production.
- Setting limits so high that they never engage, or so low that normal power users cannot work.
When to revisit
Revisit the numbers when you add a new expensive feature, when support tickets about “too many requests” appear, or when a provider bill jumps. Start conservative on cost-sensitive paths and loosen once you have real usage data. The first version does not have to be perfect; it has to exist before the first unexpected spike.
Rate limiting is one of the few controls that pays for itself the first time a script or a bot finds an unprotected endpoint. Ship a simple version early, log the hits, and adjust. That is usually enough to keep a solo SaaS from turning a quiet Tuesday into an expensive incident.