React rate limiting: Prevent API quota overages
Rate limiting is a critical but often-overlooked feature of chat applications. Without it, a single user (or a bot-controlled user) can exhaust your API quota in minutes, incurring massive costs or blocking legitimate traffic. This article covers client-side rate limiting (enforce limits in React to prevent accidental over-usage) and server-side rate limiting (enforce them in your backend to protect against abuse).
A typical OpenAI account has a rate limit of 3,500 requests per minute (as of 2026). A user sending rapid-fire messages could hit this in seconds. React rate limiting prevents the frontend from overwhelming the API, while server-side limits are your safety net.
Understanding Rate Limiting Concepts
Rate limiting uses two common patterns: token bucket and sliding window.
Token bucket simulates a bucket that fills at a fixed rate (e.g., 1 token per second). Each request costs 1 token. If the bucket is empty, the request is rejected. This allows burst traffic (if tokens accumulate) but maintains an average rate.
Sliding window counts requests in a rolling time window (e.g., the last 60 seconds). When a new request arrives, discard old requests older than the window and check if the count is under the limit. This is more precise than token bucket but slightly harder to implement.
For chat apps, token bucket is simpler and sufficient. Here is a comparison:
| Method | Pros | Cons | Best for |
|---|---|---|---|
| Token bucket | Allows bursts; simple implementation | Less precise for strict limits | Most chat apps |
| Sliding window | Exact rate enforcement | More memory overhead; complex logic | High-volume APIs |
| Fixed window | Simplest implementation | Allows burst at window boundaries | Simple use cases |
Implementing Client-Side Token Bucket Rate Limiting
Create a React hook that enforces rate limits:
import { useState, useCallback, useRef } from 'react';
function useRateLimit(tokensPerSecond = 1) {
const [tokens, setTokens] = useState(tokensPerSecond);
const [isLimited, setIsLimited] = useState(false);
const refillIntervalRef = useRef(null);
// Start refilling tokens
const startRefill = useCallback(() => {
if (refillIntervalRef.current) return;
refillIntervalRef.current = setInterval(() => {
setTokens((prev) => Math.min(prev + tokensPerSecond / 10, tokensPerSecond));
}, 100); // Refill every 100ms for smooth updates
}, [tokensPerSecond]);
// Stop refilling when component unmounts
const stopRefill = useCallback(() => {
if (refillIntervalRef.current) {
clearInterval(refillIntervalRef.current);
refillIntervalRef.current = null;
}
}, []);
// Consume a token if available
const tryConsume = useCallback(
(cost = 1) => {
startRefill();
if (tokens >= cost) {
setTokens((prev) => prev - cost);
setIsLimited(false);
return true;
}
setIsLimited(true);
return false;
},
[tokens, startRefill]
);
// Time to wait before the next request is allowed
const timeUntilReady = useCallback(() => {
if (tokens >= 1) return 0;
return ((1 - tokens) / tokensPerSecond) * 1000; // milliseconds
}, [tokens, tokensPerSecond]);
return {
isLimited,
tokens: Math.floor(tokens * 10) / 10, // Round for display
tryConsume,
timeUntilReady,
stopRefill,
};
}
export default useRateLimit;
Use this hook in your chat component:
function ChatApp() {
const [messages, setMessages] = useState([]);
const rateLimit = useRateLimit(1); // 1 message per second
const handleSendMessage = async (userMessage) => {
if (!rateLimit.tryConsume()) {
const waitMs = rateLimit.timeUntilReady();
alert(`Rate limited. Please wait ${Math.ceil(waitMs / 1000)}s.`);
return;
}
// Send message...
};
return (
<div>
{/* Chat UI */}
<div style={{ marginTop: '8px', fontSize: '12px', color: '#666' }}>
Requests available: {rateLimit.tokens.toFixed(1)} / second
</div>
</div>
);
}
This prevents the user from sending more than 1 message per second. You can adjust tokensPerSecond based on your API tier.
Server-Side Rate Limiting
For production, implement rate limiting on your backend. If you are calling OpenAI directly from React (not recommended for production), you cannot enforce server-side limits. Instead, create a backend proxy:
// Instead of calling OpenAI directly from React:
const response = await fetch('https://api.openai.com/v1/chat/completions', {
// This is insecure and reveals your API key to the browser
});
// Call your own backend:
const response = await fetch('https://yourbackend.com/api/chat', {
method: 'POST',
body: JSON.stringify({ messages, model: 'gpt-4o-mini' }),
headers: { 'Content-Type': 'application/json' },
});
Your backend then implements rate limiting before forwarding to OpenAI. Here is a simple Node.js/Express example:
const express = require('express');
const rateLimit = require('express-rate-limit');
const fetch = require('node-fetch');
const app = express();
// Rate limit: 10 requests per minute per IP
const limiter = rateLimit({
windowMs: 60 * 1000, // 1 minute
max: 10, // 10 requests
message: 'Too many requests. Please try again later.',
standardHeaders: true, // Return rate limit info in `RateLimit-*` headers
legacyHeaders: false,
});
app.use('/api/chat', limiter);
app.post('/api/chat', async (req, res) => {
try {
const response = await fetch('https://api.openai.com/v1/chat/completions', {
method: 'POST',
headers: {
'Authorization': `Bearer ${process.env.OPENAI_API_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
model: 'gpt-4o-mini',
messages: req.body.messages,
stream: true,
}),
});
// Stream the response back to the client
res.setHeader('Content-Type', 'text/event-stream');
response.body.pipe(res);
} catch (error) {
res.status(500).json({ error: error.message });
}
});
app.listen(3001);
The express-rate-limit middleware automatically enforces a limit per IP address. Clients hitting the limit receive a 429 response with headers telling them when to retry.
Handling Rate Limit Headers
AI APIs return rate limit information in response headers. Parse them in React:
async function fetchWithRateLimitTracking(url, options = {}) {
const response = await fetch(url, options);
const rateLimitLimit = response.headers.get('x-ratelimit-limit-requests');
const rateLimitRemaining = response.headers.get('x-ratelimit-remaining-requests');
const rateLimitResetDate = response.headers.get('x-ratelimit-reset-requests');
console.log({
limit: rateLimitLimit,
remaining: rateLimitRemaining,
resetDate: rateLimitResetDate,
});
if (response.status === 429) {
const retryAfter = response.headers.get('retry-after'); // seconds
const error = new Error('Rate limited');
error.retryAfter = parseInt(retryAfter) * 1000; // Convert to ms
throw error;
}
return response;
}
// Usage:
try {
const response = await fetchWithRateLimitTracking('https://api.openai.com/v1/chat/completions', {
// fetch options
});
} catch (error) {
if (error.retryAfter) {
console.log(`Retry after ${error.retryAfter}ms`);
}
}
Store the rateLimitRemaining value in React state and display it to the user. When it gets low (e.g., 5 remaining), warn the user.
Combining Client and Server Rate Limiting
Best practice is a two-layer approach: client-side rate limiting prevents accidental over-usage, and server-side limits are your safety net against abuse. Here is the architecture:
User sends message
↓
Client-side rate limiter checks: Can I send?
├─ NO → Show "Please wait X seconds" → Stop
└─ YES → Send request to backend
↓
Backend rate limiter checks: IP limit exceeded?
├─ NO → Forward to OpenAI → Stream response back
└─ YES → Return 429 with Retry-After header
↓
Client receives 429 → Exponential backoff + retry
↓
User sees "Rate limited. Retrying in Xs..."
Key Takeaways
- Token bucket is the simplest client-side rate limiting pattern for chat apps.
- Implement client-side rate limiting to prevent accidental over-usage.
- Deploy server-side rate limiting in your backend to protect against abuse.
- Parse
x-ratelimit-*headers to warn users when approaching quota limits. - Use exponential backoff when retrying after a 429 response.
Frequently Asked Questions
Should I use client-side rate limiting if I have server-side limits?
Yes. Client-side limits provide immediate feedback to the user without a network round-trip. Server-side limits are your safety net. Both together create a good UX and protect your API quota.
How do I know what rate limit to set in React?
Check your API provider's documentation for your account tier. OpenAI allows 3,500 requests per minute on most tiers. Dividing by 60 gives roughly 58 requests per second, but you should be more conservative (e.g., 1–2 per second) to avoid errors.
What if a user closes the browser mid-stream?
The fetch request cancels, and the browser stops reading the stream. Your rate limiter sees this as a completed request (the token was consumed). If you want to refund the token for cancelled requests, implement this on the server.
Can I charge users different rate limits based on subscription tier?
Yes. Pass the user's tier to your backend rate limiter, which applies tier-specific limits. For example, free users get 10 requests per minute; premium gets 1,000.
How do I reset rate limits daily or hourly?
Use a sliding window approach: track request timestamps and discard any older than the window. Or reset a counter on a schedule (e.g., every midnight UTC). Most libraries handle this automatically.