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
While global rate limits apply to all requests, per-user rate limits allow you to enforce limits specific to individual users, sessions, teams, or any other identifier.
Using the key Parameter
Pass a key to limit() to create an isolated rate limit for that specific key:
const status = await rateLimiter . limit ( ctx , "sendMessage" , {
key: userId
});
Each unique key gets its own independent rate limit bucket.
Per-User Rate Limiting
The most common pattern is to rate limit actions per authenticated user:
import { RateLimiter , MINUTE } from "@convex-dev/rate-limiter" ;
import { components } from "./_generated/api" ;
import { mutation } from "./_generated/server" ;
const rateLimiter = new RateLimiter ( components . rateLimiter , {
sendMessage: { kind: "token bucket" , rate: 10 , period: MINUTE , capacity: 3 },
});
export const sendMessage = mutation ({
args: { text: v . string () },
handler : async ( ctx , args ) => {
const user = await ctx . auth . getUserIdentity ();
if ( ! user ) {
throw new Error ( "Not authenticated" );
}
// Each user has their own rate limit
const { ok } = await rateLimiter . limit ( ctx , "sendMessage" , {
key: user . subject ,
throws: true ,
});
// Save the message
await ctx . db . insert ( "messages" , {
text: args . text ,
userId: user . subject ,
timestamp: Date . now (),
});
},
});
Real Example from Source Code
Here’s the actual example from the Convex Rate Limiter repository:
import { RateLimiter , MINUTE } from "@convex-dev/rate-limiter" ;
import { components } from "./_generated/api" ;
import { mutation } from "./_generated/server" ;
const rateLimiter = new RateLimiter ( components . rateLimiter , {
// Allows one message every ~6 seconds
// Allows up to 3 in quick succession if they haven't sent many recently
sendMessage: { kind: "token bucket" , rate: 10 , period: MINUTE , capacity: 3 },
});
export const consumeTokens = mutation ({
args: {
count: v . optional ( v . number ()),
},
handler : async ( ctx , args ) => {
const user = await ctx . auth . getUserIdentity ();
const key = user ?. subject ?? "anonymous" ;
return rateLimiter . limit ( ctx , "demoLimit" , {
count: args . count || 1 ,
key ,
});
},
});
Per-Session Rate Limiting
For rate limiting based on browser sessions or devices:
export const makeRequest = mutation ({
args: { sessionId: v . string () },
handler : async ( ctx , args ) => {
await rateLimiter . limit ( ctx , "apiRequest" , {
key: args . sessionId ,
throws: true ,
});
// Process request
},
});
Per-Team Rate Limiting
For multi-tenant applications where rate limits apply per team or organization:
export const exportData = mutation ({
args: { teamId: v . id ( "teams" ) },
handler : async ( ctx , args ) => {
// Verify user has access to the team
const team = await ctx . db . get ( args . teamId );
if ( ! team ) {
throw new Error ( "Team not found" );
}
// Rate limit per team
await rateLimiter . limit ( ctx , "dataExport" , {
key: args . teamId ,
throws: true ,
});
// Export team data
},
});
Combining with Authentication
Common patterns for authenticated vs anonymous users:
Fallback to Anonymous
const user = await ctx . auth . getUserIdentity ();
const key = user ?. subject ?? "anonymous" ;
await rateLimiter . limit ( ctx , "action" , { key });
Stricter Limits for Anonymous Users
Separate Rate Limits
Custom Count
const rateLimiter = new RateLimiter ( components . rateLimiter , {
authenticatedRequest: { kind: "token bucket" , rate: 100 , period: MINUTE },
anonymousRequest: { kind: "token bucket" , rate: 10 , period: MINUTE },
});
export const makeRequest = mutation ({
handler : async ( ctx ) => {
const user = await ctx . auth . getUserIdentity ();
if ( user ) {
await rateLimiter . limit ( ctx , "authenticatedRequest" , {
key: user . subject ,
throws: true ,
});
} else {
// Stricter limit for anonymous users
await rateLimiter . limit ( ctx , "anonymousRequest" , {
throws: true
});
}
},
});
const user = await ctx . auth . getUserIdentity ();
const count = user ? 1 : 10 ; // Anonymous requests count 10x more
await rateLimiter . limit ( ctx , "apiRequest" , {
key: user ?. subject ?? "anonymous" ,
count ,
throws: true ,
});
Key Best Practices
Keys should be stable and unique. Good choices:
User IDs from your auth system
Session IDs
Team/organization IDs
IP addresses (for anonymous users)
Avoid:
Usernames (can change)
Email addresses (can change)
Display names
Sanitize user-provided keys
If the key comes from user input, validate it: if ( args . teamId . length > 100 ) {
throw new Error ( "Invalid team ID" );
}
Each unique key creates a separate rate limit bucket in the database. Be mindful of:
How many unique keys you’ll have
Whether old keys can be cleaned up
Storage implications for high-cardinality keys
Testing Different Keys
From the source code’s test suite:
export const test = internalMutation ({
args: {},
handler : async ( ctx ) => {
// User 1 can consume tokens
const first = await rateLimiter . limit ( ctx , "sendMessage" , {
key: "user1" ,
throws: true ,
});
assert ( first . ok );
// User 2 has their own independent limit
const user2 = await rateLimiter . limit ( ctx , "sendMessage" , {
key: "user2" ,
});
assert ( user2 . ok );
},
});
Next Steps