Overview
The useRateLimit hook allows you to check rate limit status directly in your React components. This enables you to:
- Show real-time rate limit status to users
- Disable buttons when rate limited
- Display countdown timers until retry is available
- Provide better UX by checking limits client-side before sending requests
Setting Up the Server API
First, create server queries using hookAPI() to expose your rate limits:
// In convex/rateLimit.ts
import { RateLimiter, MINUTE } from "@convex-dev/rate-limiter";
import { components } from "./_generated/api";
const rateLimiter = new RateLimiter(components.rateLimiter, {
sendMessage: { kind: "token bucket", rate: 10, period: MINUTE, capacity: 3 },
});
// Export the hook API functions
export const { getRateLimit, getServerTime } = rateLimiter.hookAPI(
"sendMessage",
{
// Optionally provide a key function
key: async (ctx) => {
const identity = await ctx.auth.getUserIdentity();
if (!identity) throw new Error("Not authenticated");
return identity.subject;
},
},
);
Server API Options
The hookAPI method accepts two parameters:
- name (string): The rate limit name from your RateLimiter definition
- options (optional):
key: String or async function to determine the rate limit key
sampleShards: Number of shards to sample (if using sharding)
Key Function Patterns
1. Server-Determined Key
export const { getRateLimit, getServerTime } = rateLimiter.hookAPI(
"sendMessage",
{
// Server determines the key from auth context
key: async (ctx) => await getUserId(ctx),
},
);
2. Client-Provided Key with Validation
export const { getRateLimit, getServerTime } = rateLimiter.hookAPI(
"sendMessage",
{
// Client provides key, server validates access
key: async (ctx, keyFromClient) => {
await ensureUserCanUseKey(ctx, keyFromClient);
return keyFromClient;
},
},
);
3. Static Key
export const { getRateLimit, getServerTime } = rateLimiter.hookAPI(
"globalLimit",
{
// Use a fixed key for global rate limits
key: "global",
},
);
The getServerTime Mutation
The hookAPI returns a getServerTime mutation that helps synchronize client and server clocks:
export const { getRateLimit, getServerTime } = rateLimiter.hookAPI("sendMessage");
From the source code (client/index.ts:264-270):
getServerTime: mutationGeneric({
args: {},
returns: v.number(),
handler: async () => {
return Date.now();
},
}),
This ensures accurate retryAt calculations even when client and server clocks differ.
Using the Hook in React
Basic Usage
import { useRateLimit } from "@convex-dev/rate-limiter/react";
import { api } from "../convex/_generated/api";
function SendMessageButton() {
const { status, check } = useRateLimit(api.rateLimit.getRateLimit, {
// Recommended: sync client and server clocks
getServerTimeMutation: api.rateLimit.getServerTime,
// Number of tokens to check for
count: 1,
});
if (!status) {
return <button disabled>Loading...</button>;
}
return (
<button disabled={!status.ok}>
{status.ok ? "Send Message" : `Wait ${getCountdown(status.retryAt)}s`}
</button>
);
}
function getCountdown(retryAt: number | undefined) {
if (!retryAt) return 0;
return Math.ceil((retryAt - Date.now()) / 1000);
}
Hook Options
The useRateLimit hook accepts these options:
export type UseRateLimitOptions = {
name?: string; // Override rate limit name
key?: string; // Client-provided key (if server allows)
count?: number; // Tokens to check (default: 1)
sampleShards?: number; // Shards to sample (if sharded)
getServerTimeMutation?: GetServerTimeMutation; // For clock sync
config?: RateLimitConfig; // Inline config (advanced)
};
Return Value
The hook returns:
{
status: {
ok: boolean; // true if rate limit allows the request
retryAt?: number; // Client timestamp when retry is allowed
} | undefined,
check: (ts?: number, count?: number) => CheckResult | undefined
}
The check() Function
Use check() to get detailed rate limit information at specific times:
function DetailedStatus() {
const { status, check } = useRateLimit(api.rateLimit.getRateLimit, {
getServerTimeMutation: api.rateLimit.getServerTime,
});
// Check the status right now
const current = check(Date.now(), 1);
if (!current) return <div>Loading...</div>;
return (
<div>
<p>Available tokens: {current.value}</p>
<p>Status: {current.ok ? "Ready" : "Rate limited"}</p>
{current.retryAt && (
<p>Retry at: {new Date(current.retryAt).toLocaleTimeString()}</p>
)}
</div>
);
}
From the source (react/index.ts:81-103):
const check = useCallback(
(ts?: number, count?: number) => {
if (!rateLimitData) return undefined;
const clientTime = ts ?? Date.now();
const serverTime = clientTime + timeOffset;
const value = calculateRateLimit(
rateLimitData,
rateLimitData.config,
serverTime,
count,
);
return {
value: value.value,
ts: value.ts - timeOffset,
config: rateLimitData.config,
shard: rateLimitData.shard,
ok: value.value >= 0,
retryAt: value.retryAfter
? serverTime + value.retryAfter - timeOffset
: undefined,
};
},
[rateLimitData, timeOffset],
);
Complete Example: Message Sender
import { useState } from "react";
import { useMutation } from "convex/react";
import { useRateLimit } from "@convex-dev/rate-limiter/react";
import { api } from "../convex/_generated/api";
function MessageInput() {
const [message, setMessage] = useState("");
const sendMessage = useMutation(api.messages.send);
const { status, check } = useRateLimit(api.rateLimit.getRateLimit, {
getServerTimeMutation: api.rateLimit.getServerTime,
count: 1,
});
const handleSend = async () => {
if (!status?.ok) return;
try {
await sendMessage({ content: message });
setMessage("");
} catch (error) {
console.error("Failed to send:", error);
}
};
const countdown = status?.retryAt
? Math.ceil((status.retryAt - Date.now()) / 1000)
: 0;
return (
<div>
<input
value={message}
onChange={(e) => setMessage(e.target.value)}
placeholder="Type a message..."
/>
<button
onClick={handleSend}
disabled={!status?.ok || !message}
>
{status?.ok ? "Send" : `Wait ${countdown}s`}
</button>
{/* Show token count */}
<RateLimitStatus check={check} />
</div>
);
}
function RateLimitStatus({ check }: { check: Function }) {
const current = check(Date.now(), 0);
if (!current) return null;
return (
<div style={{ fontSize: "0.8em", color: "#666" }}>
Available: {Math.max(0, current.value)} tokens
</div>
);
}
Auto-Refreshing Status
The hook automatically refreshes when the rate limit recovers:
From the source (react/index.ts:119-123):
useEffect(() => {
if (ret?.status?.ok !== false) return;
const interval = setTimeout(refresh, ret.status.retryAt - Date.now());
return () => clearTimeout(interval);
}, [ret?.status?.ok, ret?.status?.retryAt, refresh]);
The component automatically re-renders when retryAt is reached.
Countdown Timer Example
function CountdownTimer({ retryAt }: { retryAt?: number }) {
const [now, setNow] = useState(Date.now());
useEffect(() => {
if (!retryAt) return;
const interval = setInterval(() => {
setNow(Date.now());
}, 100);
return () => clearInterval(interval);
}, [retryAt]);
if (!retryAt || now >= retryAt) return null;
const seconds = Math.ceil((retryAt - now) / 1000);
return <span>Retry in {seconds}s</span>;
}
function MyComponent() {
const { status } = useRateLimit(api.rateLimit.getRateLimit, {
getServerTimeMutation: api.rateLimit.getServerTime,
});
return (
<button disabled={!status?.ok}>
{status?.ok ? "Click me" : <CountdownTimer retryAt={status?.retryAt} />}
</button>
);
}
Multiple Token Check
Check if enough tokens are available for different actions:
function BulkActions() {
const { check } = useRateLimit(api.rateLimit.getRateLimit, {
getServerTimeMutation: api.rateLimit.getServerTime,
});
// Check different token counts
const canSendOne = check(Date.now(), 1)?.ok;
const canSendFive = check(Date.now(), 5)?.ok;
const canSendTen = check(Date.now(), 10)?.ok;
return (
<div>
<button disabled={!canSendOne}>Send 1 message</button>
<button disabled={!canSendFive}>Send 5 messages</button>
<button disabled={!canSendTen}>Send 10 messages</button>
</div>
);
}
Client-Provided Keys
When the server allows client-provided keys:
function TeamRateLimitStatus({ teamId }: { teamId: string }) {
const { status } = useRateLimit(api.rateLimit.getTeamRateLimit, {
key: teamId, // Client provides the key
getServerTimeMutation: api.rateLimit.getServerTime,
});
return (
<div>
Team rate limit: {status?.ok ? "Available" : "Exhausted"}
</div>
);
}
Best Practices
- Always use getServerTimeMutation: Ensures accurate retry times even with clock skew
- Handle loading state: The hook returns
undefined while loading
- Show countdown timers: Give users feedback on when they can retry
- Check before actions: Use the hook to enable/disable UI elements
- Combine with server checks: Client-side checks are advisory; always check server-side too
Client-side checks are not security: Always enforce rate limits on the server. The React hook is for UX only.
Type Safety
The hook is fully typed with TypeScript:
type UseRateLimitReturn = {
status: {
ok: boolean;
retryAt?: number;
} | undefined;
check: (ts?: number, count?: number) => {
value: number;
ts: number;
config: RateLimitConfig;
shard: number;
ok: boolean;
retryAt?: number;
} | undefined;
};
For more on rate limiting patterns, see Dynamic Limits for runtime configuration and Jitter for handling burst traffic.