Most SaaS APIs get rate limiting the week after an incident. A customer's integration loops, a script retries without backoff, or one tenant's bulk import saturates the database, and every other customer's requests start timing out. The limit added under that pressure is usually a single global number per API key, picked by guesswork, and it tends to cause the next problem: the largest customer's legitimate nightly sync starts failing, support escalates, and the limit is quietly raised until it protects nothing. Rate limiting is really a decision about who gets degraded when demand exceeds capacity, and it is worth making on purpose rather than in the middle of an outage.
What rate limiting is actually for
A rate limit does at least four different jobs, and conflating them into one number is why that number is always wrong. The first is protecting shared capacity from a single tenant — the noisy-neighbour problem that every multi-tenant architecture eventually meets. The second is protecting against abuse: credential stuffing on the login endpoint, scraping, and enumeration of IDs. The third is protecting downstream dependencies that have their own limits or their own per-call price, such as a payment provider or an LLM API billed by the token. The fourth is commercial — enforcing the difference between plans. Each job has a different key, a different threshold, and a different correct response when it trips, so each deserves its own limit.
Choosing the algorithm
- Fixed window counters are the simplest: a count per key per minute. Their weakness is the boundary, where a client can send a full window's allowance at the end of one minute and another at the start of the next, doubling the intended burst.
- Sliding window counters smooth that boundary by weighting the previous window, at a small cost in precision and storage. They are a sensible upgrade when fixed windows cause visible spikes.
- Token buckets allow bursts up to the bucket size while enforcing a sustained refill rate. This is the right default for most public APIs, because real clients are bursty — a sync job fires fifty requests and then goes quiet — and a token bucket accommodates that without raising the sustained ceiling.
- Leaky buckets and queues smooth output to a constant rate. They belong in front of a fragile dependency rather than at the edge of your API.
- Concurrency limits cap in-flight requests rather than requests per second. For expensive endpoints — exports, reports, search — ten slow concurrent requests do more damage than a thousand fast ones, and a concurrency limit is the tool that actually matches the cost.
What to key the limit on
Per API key is the baseline, but on its own it is easy to defeat: a customer who hits the limit creates a second key. Add a per-tenant aggregate so limits cannot be multiplied by generating credentials, and a per-user limit within the tenant for interactive traffic. Limit by IP address only on unauthenticated endpoints — login, signup, password reset — because IP-based limits on authenticated traffic punish corporate customers whose thousands of employees share a single NAT address. The biggest single improvement over naive counting is weighting by cost: a search, an export, or an AI-backed endpoint should consume more of the budget than a GET by ID. A flat request count treats a request that takes two milliseconds the same as one that holds a database connection for four seconds, and the capacity you are protecting does not see them that way.
Where to enforce it
Enforcement belongs in layers. At the edge — a CDN or WAF — apply coarse abuse limits and absorb unauthenticated floods before they reach your infrastructure. At the API gateway or middleware, enforce per-tenant quotas against a shared store; an in-memory counter in each application instance quietly multiplies the limit by the number of instances, so twelve pods each allow the full allowance. Redis with an atomic script, or the gateway's native support, is the usual answer. Then enforce inside the application at the resource itself: per-tenant concurrency on background jobs and queues, connection pool partitioning, and caps on fan-out. Decide in advance what happens when the shared store is unavailable. For authenticated tenant quotas, failing open with a conservative local fallback usually beats taking the API down; for authentication endpoints, failing closed is the safer choice.
Making limits something customers can build against
- Return HTTP 429 with a Retry-After header, and document that well-behaved clients should honour it with exponential backoff and jitter.
- Expose the current state in response headers — limit, remaining, and reset — so integrations can pace themselves before they are rejected rather than after.
- Distinguish a per-second rate limit from an exhausted monthly quota. They need different client behaviour, and returning the same error for both generates support tickets.
- Publish the limits per plan. Undocumented limits are discovered in production by your largest customers at the worst possible moment.
- Give customers alternatives to polling. Bulk endpoints and webhooks remove most of the traffic that hits limits in the first place.
- Roll out new limits in shadow mode. Log what would have been rejected for two weeks, contact the tenants who would be affected, and only then enforce. This is the step most teams skip, and it is the one that prevents the escalation.
Setting the actual numbers
Start from measured traffic rather than intuition. Take the p99 per-tenant request rate over the last thirty days for each endpoint class and set the limit at a multiple of it — three to five times is a reasonable starting point — so legitimate behaviour has headroom and pathological behaviour does not. Then check the result against capacity: the sum of plausible simultaneous bursts from your largest tenants has to fit inside what load testing shows the system can actually absorb. Make per-tenant overrides a configuration change rather than a code deploy, and give every override an expiry date, because an override granted for a one-off migration otherwise becomes a permanent exemption. Alert on 429 rates per tenant, since a customer suddenly being rejected is either an integration bug on their side or a limit that is wrong on yours, and both are worth knowing about before they email.
How Infiniti Tech Partners approaches this
This usually reaches us in one of two ways: after an incident where one tenant took the platform down for everyone, or ahead of a public API launch or an enterprise deal where the buyer has asked how the platform is protected. The work is contained and well-defined — analyse real traffic per tenant and endpoint, introduce cost-weighted limits in shadow mode, add the headers and documentation, move enforcement into a shared store, and set up per-tenant alerting — and it typically fits in a few weeks alongside the rest of a roadmap. Our bias is toward fewer, well-chosen limits that customers can see and build against, rather than a thicket of undocumented thresholds that only surface when someone's integration breaks. We keep a deliberately small number of concurrent engagements and plan the calendar about a quarter out. If a single customer's traffic has ever been an incident for you, that is worth fixing before the next one.
Frequently asked questions
What is the best rate limiting algorithm for a SaaS API?
A token bucket is the right default for most public APIs, because it allows bursts up to the bucket size while enforcing a sustained refill rate, and real clients are bursty. Fixed windows are simplest but allow double bursts at window boundaries, sliding windows smooth that, and leaky buckets belong in front of a fragile dependency. For expensive endpoints such as exports, reports, and search, add concurrency limits, since a few slow concurrent requests do more damage than many fast ones.
Should API rate limits be per API key, per user, or per IP?
Use several keys for different jobs. Per API key is the baseline, but add a per-tenant aggregate so customers cannot multiply limits by creating more keys, and a per-user limit for interactive traffic. Limit by IP only on unauthenticated endpoints like login, signup, and password reset, because IP limits on authenticated traffic punish corporate customers who share a NAT address. Weighting requests by cost, so a search or export consumes more budget than a simple read, is the biggest improvement over naive counting.
How do you introduce API rate limits without breaking customers?
Base the numbers on measured traffic, such as three to five times each tenant's p99 request rate over thirty days, and check them against load-tested capacity. Roll the limits out in shadow mode first, logging what would have been rejected for about two weeks and contacting affected tenants before enforcing. Return 429 with a Retry-After header, expose limit, remaining, and reset headers, publish limits per plan, and offer bulk endpoints and webhooks so customers do not need to poll.
Related reading
Code Review That Actually Catches Things
Most review comments are about naming while the authorization bug ships. What review is for, what to automate, and how to make it fast enough to trust.
EngineeringCustomer-Facing Analytics Without Wrecking Your Production Database
In-app dashboards are the feature most likely to take your site down. How to separate the read path, pre-aggregate, and decide whether to build or embed.
EngineeringBackground Jobs and Queues: The Half of Your System Nobody Designs
Async work quietly becomes the least reliable part of a SaaS platform. The properties that matter, the failure modes, and how to choose the technology.