Rate Limiting Next.js APIs with Upstash
September 24, 2026 Avishka Devinda
September 24, 2026 Avishka Devinda
If an API route is public, I assume a bot will eventually call it.
That does not mean every small application needs complicated infrastructure, but some endpoints should not accept unlimited requests.
For Next.js projects, I like using Upstash Redis with @upstash/ratelimit when I need a simple distributed rate limit.
I do not rate limit every request automatically.
I focus on endpoints where abuse can cost money or create problems.
Examples:
A static page does not need the same protection as an email endpoint.
A typical setup can look like:
import { Ratelimit } from '@upstash/ratelimit';
import { Redis } from '@upstash/redis';
const redis = Redis.fromEnv();
export const ratelimit = new Ratelimit({
redis,
limiter: Ratelimit.slidingWindow(5, '1 m'),
});
Then the route checks the limit before doing expensive work:
const result = await ratelimit.limit(identifier);
if (!result.success) {
return Response.json(
{ error: 'Too many requests' },
{ status: 429 }
);
}
Now the request can stop before sending an email or running an expensive query.
The rate limit needs a key.
The obvious choice is often an IP address, but I do not treat IP addresses as perfect user identities.
Many people can share an IP.
One user can also change IPs.
Depending on the route, I may use:
For authenticated actions, a user or account ID is usually more meaningful than an IP.
A contact form and a search endpoint should not necessarily have the same rule.
For example:
Contact form: very small limit
Password reset: very small limit
Normal API reads: larger limit
AI generation: tied to account/plan
The right number depends on what normal usage looks like.
I try to make the limit high enough that a real user does not notice it and low enough that obvious automation cannot run forever.
When a request exceeds the limit, I return HTTP status 429 Too Many Requests.
That is better than returning a generic 500 because the server is not broken.
The client has simply exceeded the allowed request rate.
For some APIs, I also return information about when the client can retry.
A rate limit is useful, but it does not replace:
It is one layer.
For example, a user should not be able to update another user's project just because they are under the rate limit.
Authorization still decides whether the action is allowed.
I also think about what happens if the rate-limit service is unavailable.
For some low-risk endpoints, failing open may be acceptable.
For expensive or security-sensitive actions, failing closed may be safer.
There is no universal answer.
The important thing is making that decision intentionally instead of discovering the behavior during an outage.
I like Upstash for small SaaS applications because I can add a distributed Redis-backed limit without running a Redis server myself.
The same store can also be useful for:
I still keep the rate-limit logic small and close to the boundary of the route.
Rate limiting is one of those features that is easy to ignore while building locally.
Production traffic is different.
A few lines of protection around email, authentication, and expensive API actions can prevent a lot of unnecessary work.
For small Next.js applications, Upstash gives me a practical way to add that protection without adding much infrastructure.