Rate limiting caps how many requests a client can make within a specified time. Put it in front of an AI API to reduce abuse, protect backend capacity, and control spending on external models. Provider limits are separate: an application must also keep its outbound requests within the provider’s allowance. section 9.3.2 section 8.2.2
A practical implementation has three parts:
- Run the check early in the request-processing pipeline, before core application logic. It can be composed with CORS handling, authentication, and security headers. section 9.1.2
- Track requests against a client identifier in Redis. One Upstash example uses a sliding window of five requests per ten seconds. section 9.3.2
- When the limit is exceeded, stop processing and return HTTP 429 with a “Too many requests” message. Test the behavior by generating load against the endpoint. section 9.3.2
Listing 9.5 Integrating a rate limiter in middleware
import { Ratelimit } from "@upstash/ratelimit"; #1
import { Redis } from "@upstash/redis";
const redis = new Redis({ #2
url: process.env.UPSTASH_REDIS_REST_URL,
token: process.env.UPSTASH_REDIS_REST_TOKEN,
});
const ratelimit = new Ratelimit({ #3
redis: redis,
limiter: Ratelimit.slidingWindow(5, "10 s")
});
const rateLimit = async (request, response) => {
const identifier = request.ip || '127.0.0.1'; #4
try {
const { success, limit, reset, remaining } =
await ratelimit.limit(identifier); #5
if (!success) {
console.log("Rate limit exceeded");
return {
response: NextResponse.json(
{ message: "Too many requests" }, { status: 429 }),
continue: false,
};
}
return { continue: true };
} catch (error) {
console.error("Rate limiting error:", error);
return {
response: NextResponse.json(
{ error: "Internal Server Error" }, { status: 500 }),
continue: false,
};
}
};
The middleware listing configures @upstash/ratelimit with Upstash Redis, checks an identifier derived from the request IP, and stops the chain with a 429 response when the check fails. section 9.3.2
A request-rate limit and a message quota serve related but different jobs. For an authenticated /api/chat route, a daily quota can use a Clerk user ID and a date-stamped Redis counter. The illustrated route increments the counter, rejects messages after ten for that dated key, and expires a newly created key after 24 hours. Enforce this on the backend rather than relying on the interface. section 9.3.2
Listing 9.6 Integrating message quota limits
import { getAuth } from '@clerk/nextjs/server'; #1
const checkMessageQuota = async (userId) => {
const now = new Date();
const today = now.toISOString().split('T')[0];
const key = `message_count:${userId}:${today}`; #2
const count = await redis.incr(key); #3
if (count === 1) {
await redis.expire(key, 24 * 60 * 60);
}
if (count > 10) { #4
return false;
}
return true;
};
export async function POST(req) {
try {
// rest of the code
const { userId } = getAuth(req);
const quotaAvailable = await checkMessageQuota(userId);
if (!quotaAvailable) { #5
return new Response(JSON.stringify(
{ error: 'Message quota exceeded.
You can only send 10 messages per day.' }), {
status: 429,
headers: {
'Content-Type': 'application/json',
},
});
}
// rest of the code
}
The route listing shows the user-specific counter, expiry, ten-message threshold, and 429 quota response. section 9.3.2
For public production endpoints, consider an external control such as Cloudflare, AWS WAF, or a dedicated Redis cluster so abusive traffic can be stopped before it consumes application-server resources. Rate limiting remains one layer alongside authentication, input validation, and other security controls; document-upload endpoints are another candidate for limits and quotas. section 9.3.2 section 11.2.2













