Rate limiting is one of those decisions that looks trivial until it isn’t. You ship your API, things are going fine, then one day a customer’s integration goes haywire, hammers your endpoints, and your database falls over for everyone else. So you slap on a rate limiter. You pick a number – 100 requests per minute, say – and call it done. Six months later your best enterprise customer is hitting that ceiling during legitimate peak usage and lodging a support ticket that ends in a contract conversation you didn’t want to have.
The mistake isn’t adding rate limiting. The mistake is treating it as a blunt instrument when it needs to be a scalpel. Getting this right early – before your user base is big enough to cause real damage – is one of the few architecture decisions that pays dividends immediately and compounds over time.
Why Most SaaS Rate Limiting Fails in Practice
The default approach is global rate limiting at the API gateway layer. One rule, applied uniformly, across all customers and all endpoints. It’s easy to implement and it does prevent the worst abuse scenarios. But it creates three problems that don’t show up until you have paying customers.
First, it treats your highest-value customers the same as your lowest-value ones. A startup on your $49/month plan and an enterprise on $4,000/month will hit the same ceiling. That’s a commercial problem masquerading as a technical one.
Second, uniform limits don’t account for endpoint cost. A GET request that returns cached data is not the same as a POST that triggers a multi-step workflow, hits three downstream APIs, and writes to six tables. Counting both as “one request” is architecturally dishonest.
Third, fixed windows are gameable. If your limit resets at the top of every minute, a client can fire 100 requests at 11:59 and another 100 at 12:00 and you’ve just absorbed 200 requests in two seconds. Fixed windows create spike vulnerability. Sliding windows or token bucket algorithms handle this properly, but most tutorials still demonstrate fixed windows because they’re simpler to explain.
The Tiered Model: Matching Limits to Commercial Reality
When we build rate limiting into a SaaS at Amora, the first question we ask is: what does your pricing model look like, and what does high-value usage actually look like for your best customers?
The answer to that question shapes everything. If your enterprise tier is paying for volume, then your rate limits need to reflect that commercially, not just technically. Here’s a tiered structure we use as a starting point:
- Free / trial tier: Tight limits, short windows. 30-60 requests per minute per user, with a daily cap. The goal here isn’t revenue protection – it’s abuse prevention and cost control.
- Core paid tier: Moderate limits with burst allowance. 200-300 requests per minute with a token bucket that allows short bursts up to 2x the base rate. This covers 95% of normal integration behaviour.
- Enterprise tier: Negotiated limits, often endpoint-specific, with dedicated rate limit headers so their engineers can build against known constraints. Some enterprise contracts include a formal rate limit SLA.
The tiered model only works if your rate limiter knows which tier a request belongs to. That means authenticating before rate limiting – not the other way around. If you rate limit unauthenticated requests at the same level as authenticated ones, you’re exposing your limits to enumeration and your legitimate users to collateral throttling during bot activity.
Choosing Your Algorithm: Token Bucket vs Sliding Window
This is where a lot of SaaS teams get paralysed by options. Here’s the honest summary.
Fixed window is easy to implement and easy to reason about. It’s also the weakest protection against spikes. Fine for internal tooling. Not good enough for a public API with real traffic.
Sliding window log is accurate – it tracks every request timestamp and calculates the count over a true rolling window. The problem is memory cost. If you have 50,000 active API users, storing per-user request logs in Redis gets expensive fast. We’ve seen this become a real cost centre for SaaS products with high API call volume.
Sliding window counter is a good middle ground. It uses two fixed windows and a weighted calculation to approximate sliding behaviour at a fraction of the memory cost. Accuracy drops slightly at window boundaries, but in practice the difference is negligible for typical SaaS workloads.
Token bucket is our default recommendation for most SaaS APIs. Each user gets a bucket that refills at a steady rate. Bursts are allowed up to the bucket capacity. The algorithm naturally handles the “legitimate peak usage” problem because it rewards users who aren’t constantly at the ceiling – they accumulate tokens they can spend during high-activity periods. It’s also conceptually clean enough that you can explain it to a customer’s engineering team without a whiteboard session.
Redis with Lua scripting handles token bucket logic atomically at scale. If you’re already running Redis for caching or sessions, the infrastructure cost of adding rate limiting is minimal. If you’re not, it’s a line item worth adding – Redis is not expensive, and the alternative is fighting race conditions in your database layer.
Rate Limiting at the Right Layer (And Why You Probably Need Two)
A question we get often: should rate limiting live at the API gateway, in application code, or both?
Both. But for different reasons.
Gateway-level rate limiting (NGINX, AWS API Gateway, Cloudflare, whatever sits in front of your application) handles the raw abuse scenarios – bots, credential stuffing, runaway scripts. It’s cheap to compute because it operates before your application stack gets involved. Set conservative limits here. The goal is protecting infrastructure, not enforcing commercial policy.
Application-level rate limiting – in your actual codebase, with knowledge of the authenticated user and their tier – is where commercial policy lives. This is where you enforce the tiered limits, where you account for endpoint cost weighting, and where you generate the rate limit response headers that your customers’ engineers will actually read.
The two layers serve different masters. Conflating them into a single rule creates a system that does neither job well. We’ve inherited codebases where someone put all the rate limiting logic into a single NGINX config and wondered why enterprise customers were getting throttled during demos.
What to Put in Your Rate Limit Headers (And Why This Matters Commercially)
This is the most overlooked part of rate limiting, and it directly affects customer satisfaction and support volume.
When you return a 429 Too Many Requests response, you should be including headers that tell the client exactly what happened and what to do next. The standard set:
X-RateLimit-Limit– the ceiling for this endpoint and tierX-RateLimit-Remaining– requests left in the current windowX-RateLimit-Reset– Unix timestamp when the window resets or the bucket refills to capacityRetry-After– seconds until the client should retry (required by RFC 6585, often omitted)
These headers let a well-behaved client implement exponential backoff without a support ticket. A fintech we shipped last year reduced rate-limit-related support requests by more than half simply by adding proper headers – their customers’ engineering teams could self-diagnose and adjust their integration patterns without involving anyone on the SaaS side.
Document these headers in your API reference. If you treat rate limiting as an internal implementation detail, your customers will treat hitting limits as a bug rather than expected behaviour they can work around.
Building for Exceptions: Allowlists, Override Keys, and Emergency Bypasses
You will need to break your own rules at some point. A customer is running a time-sensitive data migration. Your own internal jobs are hitting API endpoints. A partner integration legitimately needs higher throughput during a scheduled batch process.
Build the escape valves in from the start. This doesn’t need to be complicated:
- Allowlisted API keys that bypass rate limiting entirely – used for internal service accounts, never issued to customers.
- Elevated keys with higher tier limits – issued to enterprise customers during onboarding or via a support process, with an audit trail.
- Scheduled window exceptions – the ability to temporarily raise limits for a specific customer during a defined time window, without code changes. This is a configuration change in your rate limiter, not a deployment.
The alternative to building this deliberately is handling every exception as an emergency – manual database edits, config changes under pressure, or just disabling rate limiting for someone and forgetting to re-enable it. We’ve cleaned up the aftermath of that approach more than once.
If you’re building this out now and not sure where to start, or you’ve got an existing API that needs this layered in properly, talk to Amora about your build – we can scope what needs to change and how quickly it can be done without disrupting what’s already in production.
Rate limiting done well is invisible to your good customers and effective against the bad ones. That’s exactly how it should be. The teams that get this right are the ones who stopped thinking about it as a protection mechanism and started treating it as a product decision.
Got something you want built?
Amora Digital is an Australian software and AI agency. We scope it, build it, and ship it – live in 28 days. No offshore teams. No surprises.