Leave Localhost logoLeave LocalhostDocs
Security

Rate Limiting

Server-side rate limiting on abuse-prone surfaces using the Convex rate-limiter component, configured in one place in rateLimit.ts.

Abuse-prone backend surfaces are rate-limited with the @convex-dev/rate-limiter component. Limits are enforced inside Convex, so they cannot be bypassed from the client. All limits live in one place: rateLimitConfig in packages/backend/convex/rateLimit.ts.

What is limited

LimitSurfacePolicy
sensitiveEmailChallengeCreateRequesting an email verification code for a sensitive action3 per 10 minutes, keyed per user + action
sensitivePasswordConfirmWrong-password attempts during step-up5 per 15 minutes, keyed per user + action
generateUploadUrlRequesting a file-upload URLToken bucket, 20 per hour with a burst capacity of 5

The two sensitive* limits back Sensitive Action Protection; a 60-second resend cooldown for email codes is enforced inline in addition to the limit.

How it is used

A limited handler calls the shared limiter and returns a friendly message when the caller is over budget:

import { getRateLimitErrorMessage, rateLimiter } from "./rateLimit";

const limit = await rateLimiter.limit(ctx, "sensitivePasswordConfirm", {
  key: `${userId}:${action}`,
});
if (!limit.ok) {
  return { ok: false, error: getRateLimitErrorMessage(limit.retryAfter) };
}

getRateLimitErrorMessage turns the limiter's retryAfter into a human message ("Please try again in N seconds").

Adding or tuning a limit

  1. Add an entry to rateLimitConfig with a kind ("fixed window" or "token bucket"), rate, and period (use the MINUTE / HOUR helpers). shards spreads a high-volume global limit across documents.
  2. Call rateLimiter.limit(ctx, "<name>", { key }) in the handler before doing the work, choosing a key that scopes the limit (per user, per email, etc.).
  3. Return getRateLimitErrorMessage(retryAfter) on rejection.

The configured names are type-checked, so a typo in a limit() call fails at compile time.

On this page