Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/get-convex/rate-limiter/llms.txt

Use this file to discover all available pages before exploring further.

Overview

The check() method checks a rate limit’s current status without consuming any tokens. This is useful for previewing whether a request would be allowed, or for checking rate limits in queries where you cannot consume tokens.

Method Signature

async check<Name extends string = keyof Limits & string>(
  ctx: RunQueryCtx,
  name: Name,
  ...options: Name extends keyof Limits & string
    ? [WithKnownNameOrInlinedConfig<Limits, Name, RateLimitArgs>?]
    : [WithKnownNameOrInlinedConfig<Limits, Name, RateLimitArgs>]
): Promise<RateLimitReturns>

Key Difference from limit()

Unlike limit(), the check() method:
  • Does not consume tokens - It only reads the current state
  • Can be used in queries - Since it doesn’t mutate state, it works with runQuery
  • Returns the same shape - The return type is identical to limit()
This makes check() perfect for UI indicators, permission checks, or deciding whether to attempt an operation.

Parameters

ctx
RunQueryCtx
required
The context object from a query or mutation, including runQuery. Unlike limit(), this works with query contexts.
name
string
required
The name of the rate limit to check. If this name was defined in the RateLimiter constructor, it will be type-checked and auto-completed.
options
object
Optional configuration for this rate limit check.
key
string
A unique identifier for this rate limit instance. Use this to check per-user, per-IP, or other scoped rate limits.
count
number
default:"1"
The number of tokens to check for availability. The check will return whether this many tokens are available without consuming them.
reserve
boolean
default:"false"
If true, checks whether tokens can be reserved for future use. Returns retryAfter indicating when the reserved work should execute.
config
RateLimitConfig
The rate limit configuration. Required only if the rate limit name was not defined in the RateLimiter constructor.
The throws option is not available for check() since it’s typically used for non-blocking status checks.

Return Type

RateLimitReturns
object
Returns a promise that resolves to one of two shapes:When rate limit is not exceeded:
ok
true
required
Indicates the request would be allowed.
retryAfter
number
Only present when reserve: true. The duration in milliseconds to wait before executing the reserved work.
When rate limit is exceeded:
ok
false
required
Indicates the rate limit would be exceeded.
retryAfter
number
required
The duration in milliseconds when retrying could succeed.

Use Cases

Preview Before Action

Check if an action would be rate-limited before attempting it:
import { mutation } from "./_generated/server";
import { rateLimiter } from "./rateLimiter";

export const sendMessage = mutation({
  args: { text: v.string() },
  handler: async (ctx, args) => {
    // Check first
    const canSend = await rateLimiter.check(ctx, "sendMessage", {
      key: ctx.userId,
    });
    
    if (!canSend.ok) {
      return {
        error: `Please wait ${Math.ceil(canSend.retryAfter / 1000)} seconds`,
      };
    }
    
    // Now consume and send
    await rateLimiter.limit(ctx, "sendMessage", { key: ctx.userId });
    await ctx.db.insert("messages", { text: args.text });
  },
});

Query for UI Status

Use in a query to show rate limit status in your UI:
import { query } from "./_generated/server";
import { rateLimiter } from "./rateLimiter";

export const canUserSendMessage = query({
  handler: async (ctx) => {
    const userId = await getCurrentUser(ctx);
    
    const status = await rateLimiter.check(ctx, "sendMessage", {
      key: userId,
    });
    
    return {
      canSend: status.ok,
      retryAfter: status.ok ? undefined : status.retryAfter,
    };
  },
});

Batch Operations

Check if multiple operations would succeed before starting:
export const bulkSendEmails = mutation({
  args: { recipientIds: v.array(v.id("users")) },
  handler: async (ctx, args) => {
    // Check all recipients first
    const checks = await Promise.all(
      args.recipientIds.map((id) =>
        rateLimiter.check(ctx, "sendEmail", { key: id })
      )
    );
    
    const blockedRecipients = args.recipientIds.filter(
      (_, i) => !checks[i].ok
    );
    
    if (blockedRecipients.length > 0) {
      return {
        error: `${blockedRecipients.length} recipients are rate limited`,
        blockedRecipients,
      };
    }
    
    // All clear, proceed with actual sending
    for (const recipientId of args.recipientIds) {
      await rateLimiter.limit(ctx, "sendEmail", { key: recipientId });
      await sendEmail(ctx, recipientId);
    }
  },
});

Cost Estimation

Check if a large operation would be allowed:
export const estimateUploadCost = query({
  args: { fileSizeMB: v.number() },
  handler: async (ctx, args) => {
    const userId = await getCurrentUser(ctx);
    const tokensNeeded = Math.ceil(args.fileSizeMB);
    
    const status = await rateLimiter.check(ctx, "upload", {
      key: userId,
      count: tokensNeeded,
    });
    
    return {
      allowed: status.ok,
      tokensNeeded,
      waitTimeSeconds: status.ok ? 0 : Math.ceil(status.retryAfter / 1000),
    };
  },
});

Notes

Since check() doesn’t consume tokens, calling it multiple times won’t affect the rate limit state. This makes it safe to use in queries and for UI status indicators.
When using check() followed by limit() in the same mutation, there’s a small time gap between the check and the actual consumption. The rate limit state could change between these calls. Use check() for guidance, not as a guarantee.
Always call limit() to actually enforce the rate limit. Using only check() without limit() will not consume tokens and won’t prevent rate limit violations.