Rate Limiting
Resolve rate limit errors and configuration issues
Rate limiting issues occur when limits are misconfigured or exceeded. Check error codes, verify assertLimitAction()
calls, and adjust limits as needed.
RATE_LIMITED Error
Symptoms: Convex actions throw RATE_LIMITED
error. HTTP responses return 429 status.
Diagnosis: User exceeded configured rate limit for the action.
Solution:
Rate limits are intentional. Options:
- Increase limit (if legitimate traffic):
await assertLimitAction(ctx, {
scope: "support:create",
viewerId: ctx.viewerId,
max: 10, // Increase from 5 to 10
windowMs: 60_000,
});
- Show user-friendly error:
try {
await ctx.runAction(api.support.create, { title, body });
} catch (error) {
if (error.code === "RATE_LIMITED") {
toast.error("Too many requests. Please wait before trying again.");
}
}
- Implement exponential backoff (client-side):
async function retryWithBackoff(fn, maxRetries = 3) {
for (let i = 0; i < maxRetries; i++) {
try {
return await fn();
} catch (error) {
if (error.code === "RATE_LIMITED" && i < maxRetries - 1) {
await new Promise(r => setTimeout(r, Math.pow(2, i) * 1000));
continue;
}
throw error;
}
}
}
Rate Limiting Not Working
Symptoms: Users can spam actions without being rate limited.
Diagnosis: assertLimitAction()
not called in action or using wrong function type.
Solution:
Rate limiting only works in userAction
(not userMutation
):
// ✅ Correct - uses userAction
export const send = userAction({
handler: async (ctx, { body }) => {
await assertLimitAction(ctx, {
scope: "messages:send",
viewerId: ctx.viewerId,
max: 20,
windowMs: 60_000,
});
await ctx.runMutation(api.messages._insert, { body });
},
});
// ❌ Wrong - userMutation cannot use assertLimitAction
export const send = userMutation({
handler: async (ctx, { body }) => {
await assertLimitAction(ctx, { /* ... */ }); // Error!
},
});
Different Users Share Rate Limit
Symptoms: User A gets rate limited by User B's actions.
Diagnosis: Rate limit scope doesn't include user identifier.
Solution:
Always include viewerId
in scope:
// ✅ Correct - per-user limit
await assertLimitAction(ctx, {
scope: `messages:send:${ctx.viewerId}`, // or just use viewerId param
viewerId: ctx.viewerId,
max: 10,
windowMs: 60_000,
});
// ❌ Wrong - global limit shared by all users
await assertLimitAction(ctx, {
scope: "messages:send", // No user identifier
viewerId: "global",
max: 10,
windowMs: 60_000,
});
Rate Limit Not Resetting
Symptoms: Rate limit persists beyond configured window.
Diagnosis: Old rate limit records not cleaning up or window calculation incorrect.
Solution:
Rate limit cleanup happens via cron. Verify cron is scheduled:
import { cronJobs } from "convex/server";
import { internal } from "./_generated/api";
const crons = cronJobs();
crons.interval(
"cleanup rate limits",
{ minutes: 5 },
internal.rateLimits.cleanupExpired
);
export default crons;
Check Convex dashboard → Crons to verify job is running.
Related Documentation
- Security Overview - Rate limiting architecture
- Rate Limiting Guide - Configuration patterns