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 hookAPI() method creates a public query and mutation that can be used with the useRateLimit React hook. It returns two functions: getRateLimit for fetching current rate limit values, and getServerTime for client/server time synchronization.

Method Signature

class RateLimiter<Limits> {
  hookAPI<DataModel extends GenericDataModel, Name extends string>(
    name: Name,
    options?: HookOptions<DataModel>
  ): {
    getRateLimit: QueryFunction;
    getServerTime: MutationFunction;
  }
}

Type Definitions

type HookOptions<DataModel> = {
  key?: string | ((ctx: GenericQueryCtx<DataModel>, keyFromClient?: string) => string | Promise<string>);
  sampleShards?: number;
};

Parameters

name
string
required
The name of the rate limit to expose. Must match a rate limit defined in your RateLimiter constructor, unless you provide an inline config.
options
HookOptions
Optional configuration for the hook API.

Return Value

getRateLimit
QueryFunction
A Convex public query that returns the current rate limit value and metadata.Arguments:
{
  name?: string;        // Override the rate limit name
  key?: string;         // Client-provided key (only if allowed)
  sampleShards?: number; // Override sampleShards
  config?: RateLimitConfig; // Inline config
}
Returns:
{
  value: number;        // Current token value
  ts: number;          // Server timestamp
  shard: number;       // Shard number used
  config: RateLimitConfig; // Rate limit configuration
}
getServerTime
MutationFunction
A Convex public mutation that returns the current server time (Date.now()). Used by useRateLimit to synchronize client and server clocks.Arguments: NoneReturns: number - Server timestamp in milliseconds

Usage Patterns

// convex/messages.ts
import { rateLimiter } from "./rateLimiter";
import { getUserId } from "./auth";

export const { getRateLimit, getServerTime } = rateLimiter.hookAPI(
  "sendMessage",
  {
    // Server determines the key based on authenticated user
    key: async (ctx) => {
      const userId = await getUserId(ctx);
      if (!userId) throw new Error("Not authenticated");
      return userId;
    },
  }
);
// React component
import { useRateLimit } from "@convex-dev/rate-limiter/react";
import { api } from "./convex/_generated/api";

function MyComponent() {
  const { status } = useRateLimit(api.messages.getRateLimit, {
    getServerTimeMutation: api.messages.getServerTime,
  });
  
  // ...
}

Pattern 2: Static Key (Global Rate Limit)

// convex/freeTrials.ts
export const { getRateLimit, getServerTime } = rateLimiter.hookAPI(
  "freeTrialSignUp",
  {
    // All clients share this rate limit
    key: "global",
  }
);

Pattern 3: Client-Provided Key (Use with Caution)

// convex/multiTenant.ts
export const { getRateLimit, getServerTime } = rateLimiter.hookAPI(
  "organizationAPI",
  {
    key: async (ctx, keyFromClient) => {
      // IMPORTANT: Validate the client can access this key
      const userId = await getUserId(ctx);
      const userOrgs = await ctx.db
        .query("memberships")
        .withIndex("by_user", (q) => q.eq("userId", userId))
        .collect();
      
      if (!userOrgs.some(m => m.orgId === keyFromClient)) {
        throw new Error("Access denied to this organization");
      }
      
      return keyFromClient;
    },
  }
);
// React component
function OrganizationDashboard({ orgId }) {
  const { status } = useRateLimit(api.multiTenant.getRateLimit, {
    key: orgId,  // Client provides the org ID
    getServerTimeMutation: api.multiTenant.getServerTime,
  });
  
  // ...
}
Security Consideration: When using client-provided keys, always validate that the authenticated user has permission to access that key’s rate limit. Without validation, malicious clients could check rate limits for other users or organizations.

Complete Setup Example

Server Setup

// convex/rateLimiter.ts
import { RateLimiter, MINUTE } from "@convex-dev/rate-limiter";
import { components } from "./_generated/api";

export const rateLimiter = new RateLimiter(components.rateLimiter, {
  sendMessage: { kind: "token bucket", rate: 10, period: MINUTE, capacity: 3 },
});
// convex/messages.ts
import { rateLimiter } from "./rateLimiter";
import { mutation } from "./_generated/server";
import { getUserId } from "./auth";

// Export the hook API functions
export const { getRateLimit, getServerTime } = rateLimiter.hookAPI(
  "sendMessage",
  {
    key: async (ctx) => await getUserId(ctx),
  }
);

// Your mutation that enforces the rate limit
export const send = mutation({
  args: { /* ... */ },
  handler: async (ctx, args) => {
    const userId = await getUserId(ctx);
    
    // Enforce rate limit
    await rateLimiter.limit(ctx, "sendMessage", {
      key: userId,
      throws: true,
    });
    
    // Send the message
    // ...
  },
});

Client Setup

// App.tsx
import { useRateLimit } from "@convex-dev/rate-limiter/react";
import { useMutation } from "convex/react";
import { api } from "./convex/_generated/api";

function SendMessageForm() {
  const sendMessage = useMutation(api.messages.send);
  const { status } = useRateLimit(api.messages.getRateLimit, {
    getServerTimeMutation: api.messages.getServerTime,
    count: 1,
  });

  const handleSubmit = async (e) => {
    e.preventDefault();
    
    if (!status?.ok) {
      alert("Rate limit exceeded. Please wait.");
      return;
    }
    
    try {
      await sendMessage({ /* ... */ });
    } catch (error) {
      if (isRateLimitError(error)) {
        alert(`Rate limited. Retry in ${error.data.retryAfter}ms`);
      }
    }
  };

  return (
    <form onSubmit={handleSubmit}>
      <button type="submit" disabled={!status?.ok}>
        {status?.ok ? "Send" : "Rate limited"}
      </button>
    </form>
  );
}

Key Function Patterns

Authentication-Based

key: async (ctx) => {
  const identity = await ctx.auth.getUserIdentity();
  if (!identity) throw new Error("Not authenticated");
  return identity.subject;
}

IP-Based (requires custom auth setup)

key: async (ctx) => {
  // Assumes you've set up IP address in auth context
  return ctx.auth.getClientIP();
}

Multi-Tenant with Validation

key: async (ctx, keyFromClient) => {
  if (!keyFromClient) {
    // Default to user's personal workspace
    return await getUserId(ctx);
  }
  
  // Validate access to requested workspace
  await ensureUserCanAccessWorkspace(ctx, keyFromClient);
  return keyFromClient;
}

Build docs developers (and LLMs) love