Authentication and Permissions
Rate Limits
You can add rate limiting to your app with the rateLimit object in the permission rules. Rate limits let you control how often users can perform actions like creating records or querying data.
#Defining rate limits
Rate limits are configured in the $rateLimits key of your permissions. Each rate limit has a name and a configuration with a limits array.
// instant.perms.tsimport type { InstantRules } from '@instantdb/react';const rules = {todos: {allow: {create: 'rateLimit.createTodos.limit(auth.id)',},},$rateLimits: {createTodos: {limits: [{capacity: 10,refill: {amount: 10,period: '1 hour',},},],},},} satisfies InstantRules;export default rules;
In this example, each user can create at most 10 todos per hour. Once they hit the limit, further creates will be rejected until tokens refill.
#How it works
Rate limits use a token bucket algorithm. Each bucket starts full at its capacity. Every time a rule calls rateLimit.bucketName.limit(key), one token is consumed from the bucket for that key. When the bucket is empty, the request is rejected with a 429 error. Tokens refill over time based on your configuration.
The key argument (e.g. auth.id) determines who the limit applies to. Different keys get independent buckets. For example, rateLimit.createTodos.limit(auth.id) gives each user their own rate limit. To rate-limit by IP address, use rateLimit.createTodos.limit(request.ip).
#Rate limits are per entity
Rate limit rules are evaluated per entity, just like other permission rules. For create, update, and delete, the rule runs once for each entity in the transaction. For view, the rule runs once for each entity in the query result.
This means a query that returns 50 rows will consume 50 tokens. Keep this in mind when setting your capacity — it should account for the number of entities your queries typically return, not just the number of requests.
#Configuration
Each entry in the limits array accepts:
| Field | Required | Default | Description |
|---|---|---|---|
capacity | Yes | Maximum number of tokens in the bucket. | |
refill.amount | No | Same as capacity | Number of tokens added per refill. |
refill.period | No | "1 hour" | How often tokens refill. Accepts durations like "30 minutes", "1 day", "2 hours". Must be between 1 second and 24 hours. |
refill.type | No | "greedy" | Either "greedy" (tokens refill continuously) or "interval" (tokens refill all at once at the end of each period). |
#Greedy vs interval refill
With greedy refill (the default), tokens trickle in continuously. If your capacity is 60 with a period of "1 hour", you get roughly one token per minute.
With interval refill, all tokens are added at once when the period elapses. If your capacity is 60 with a period of "1 hour", you get all 60 tokens back at the end of one hour.
#Using rate limits in rules
#Basic usage
Call rateLimit.bucketName.limit(key) in any allow rule. It consumes a token and returns true if the request is allowed, or throws a rate limit error if the bucket is empty. You can optionally pass a second argument to consume multiple tokens: rateLimit.bucketName.limit(key, 5).
{"messages": {"allow": {"create": "rateLimit.sendMessage.limit(auth.id)"}},"$rateLimits": {"sendMessage": {"limits": [{"capacity": 5,"refill": { "period": "1 minute" }}]}}}
#Combining with other rules
Since limit returns true on success, you can combine it with other permission checks using &&:
{"messages": {"allow": {"create": "auth.id != null && rateLimit.sendMessage.limit(auth.id)"}},"$rateLimits": {"sendMessage": {"limits": [{"capacity": 20,"refill": { "period": "1 hour" }}]}}}
Put rateLimit.limit(...) last in your && chain. CEL short-circuits, so if an earlier check fails, no token is consumed.
#Rate limiting queries
You can rate limit view rules the same way:
{"messages": {"allow": {"view": "rateLimit.readMessages.limit(auth.id)"}},"$rateLimits": {"readMessages": {"limits": [{"capacity": 100,"refill": { "period": "1 hour" }}]}}}
#Consuming multiple tokens
You can consume more than one token per request by passing a second argument:
{"uploads": {"allow": {"create": "rateLimit.uploadLimit.limit(auth.id, 5)"}},"$rateLimits": {"uploadLimit": {"limits": [{"capacity": 100,"refill": { "period": "1 day" }}]}}}
#Multiple limits
You can apply multiple limits to the same bucket. For example, a burst limit and a sustained limit:
{"$rateLimits": {"sendMessage": {"limits": [{"capacity": 5,"refill": { "period": "1 minute", "type": "interval" }},{"capacity": 100,"refill": { "period": "1 day" }}]}}}
This allows a burst of 5 messages per minute, but no more than 100 per day.
#Rate limiting by different keys
The key you pass to limit determines the granularity. Here are some common patterns:
{"posts": {"allow": {"create": "rateLimit.createPosts.limit(auth.id)"}}}
Rate limit per user with auth.id. Each user gets their own bucket.
{"posts": {"allow": {"create": "rateLimit.createPosts.limit(request.ip)"}}}
Rate limit by IP address with request.ip. Useful for limiting unauthenticated requests.
#Error handling
When a rate limit is exceeded, Instant returns an error with type: "rate-limited" and a hint containing retry-after (seconds until the bucket refills):
{"type": "rate-limited","message": "Your request exceeded the rate limit.","hint": {"retry-at": "2026-04-14T12:01:00Z","retry-after": 12,"remaining-tokens": 0}}
For transactions, the error is thrown as an InstantAPIError. You can catch it and show a message to the user:
try {await db.transact(db.tx.messages[id()].update({ text: 'hello' }));} catch (e) {if (e.body?.type === 'rate-limited') {const retryAfter = e.body.hint['retry-after'];alert(`Too fast! Try again in ${retryAfter} seconds.`);}}
For queries, rate limit errors will appear in the error field returned by useQuery:
const { data, error } = db.useQuery({ messages: {} });if (error?.body?.type === 'rate-limited') {// handle rate limit}