Skip to main content

Overview

The rate limiting system uses a sliding time-window approach to prevent abuse and ensure fair resource usage. Rate limits are stored in the rateLimit database table and enforced at the guard function level before any database operations.

Database Schema

rateLimit Table

Table ID: ratelimit Database: cric_talk (ID: 695761d00008fd927f78) Row Security: Disabled (server-side access only)

Columns

ColumnTypeRequiredConstraintsDescription
userIdstringYesMax 36 charsUser identifier
activitystringYesMax 36 charsAction name (e.g., “create_post”)
windowKeyintegerYes-Time window identifier
activityCountintegerYesMin: 1, Max: 60Number of actions in current window

Indexes

IndexTypeColumnsPurpose
userid_indexkeyuserId ASCFast user lookups
activity_indexkeyactivity ASCFilter by action type
windowKey_indexkeywindowKey ASCTime window queries
createdAt_indexkey$createdAt DESCCleanup old entries

How Rate Limiting Works

1. Window Key Generation

Rate limits use 1-minute time windows. The window key is calculated as:
const windowKey = Math.floor(Date.now() / 60000);
Example:
  • 2024-01-15 10:30:00windowKey: 28953450
  • 2024-01-15 10:30:59windowKey: 28953450 (same window)
  • 2024-01-15 10:31:00windowKey: 28953451 (new window)
This creates discrete 60-second windows that automatically expire.

2. Activity Tracking

Each action type is tracked separately using a unique activity identifier:
ActivityRate LimitUsed In Function
create_post10/minposts-guard
update_post10/minposts-guard
delete_post10/minposts-guard
add_comment60/mincomments-guard
update_comment60/mincomments-guard
delete_comment60/mincomments-guard
create_room10/minrooms-guard
update_room10/minrooms-guard
delete_room10/minrooms-guard
create_room_message60/minroom-message-guard
update_room_message60/minroom-message-guard
delete_room_message60/minroom-message-guard

3. Rate Limit Check Flow

From functions/posts-guard/src/main.js:36-85:
async function rateLimitCheck(activity) {
  // 1. Calculate current time window
  const windowKey = Math.floor(Date.now() / 60000);
  
  // 2. Query for existing activity record
  const userActivity = await tablesDB.listRows({
    databaseId: CRIC_TALK_DATABASE_ID,
    tableId: RATE_LIMIT_TABLE_ID,
    queries: [
      Query.equal('userId', userId),
      Query.equal('activity', activity),
      Query.equal('windowKey', windowKey),
      Query.limit(1),
    ],
  });

  // 3. First request in this window - create tracking record
  if (userActivity.rows.length === 0) {
    await tablesDB.createRow({
      databaseId: CRIC_TALK_DATABASE_ID,
      tableId: RATE_LIMIT_TABLE_ID,
      rowId: ID.unique(),
      data: {
        userId,
        activity,
        windowKey,
        activityCount: 1,
      },
    });
    return; // Allow the request
  }

  // 4. Check if user exceeded limit
  const row = userActivity.rows[0];
  if (row.activityCount >= 10) { // Limit varies by activity
    throw new Error(
      'Rate limit exceeded: Too many requests in a short period.'
    );
  }

  // 5. Increment counter for this window
  await tablesDB.incrementRowColumn({
    databaseId: CRIC_TALK_DATABASE_ID,
    tableId: RATE_LIMIT_TABLE_ID,
    rowId: row.$id,
    column: 'activityCount',
    value: 1,
  });
}

Rate Limit Configuration

Posts Guard (10 requests/minute)

From functions/posts-guard/src/main.js:
async function createPost() {
  await rateLimitCheck('create_post');
  // ... create post logic
}

async function updatePost() {
  await rateLimitCheck('update_post');
  // ... update post logic
}

async function deletePost() {
  await rateLimitCheck('delete_post');
  // ... delete post logic
}
Rate Limit Check: row.activityCount >= 10 Actions NOT Rate Limited:
  • like - Allows unlimited likes/unlikes
  • view - Allows unlimited view tracking

Comments Guard (60 requests/minute)

From functions/comments-guard/src/main.js:60:
if (userActivity.rows[0].activityCount >= 60)
  throw new Error(
    'Rate limit exceeded: Too many requests. Please try again later.'
  );
Why Higher Limit:
  • Comments are shorter, faster to create
  • Live discussions require higher throughput
  • Less resource-intensive than posts with images

Rooms Guard (10 requests/minute)

From functions/rooms-guard/src/main.js:73:
if (row.activityCount >= 10) {
  throw new Error(
    'Rate limit exceeded: Too many requests in a short period'
  );
}
Rationale:
  • Rooms are high-value resources
  • Creation requires more validation
  • Lower limit prevents spam room creation

Room Messages Guard (60 requests/minute)

From functions/room-message-guard/src/main.js:76:
if (row.activityCount >= 60) {
  throw new Error(
    'Rate limit exceeded: Too many requests in a short period'
  );
}
Why Higher Limit:
  • Real-time chat requires fast message delivery
  • Users need to send multiple messages quickly during live matches
  • Similar to comments in usage pattern

Handling Rate Limit Errors

Client-Side Detection

Rate limit errors are thrown as exceptions:
try {
  await executePost({
    action: "create",
    content: "My cricket analysis...",
  });
} catch (error) {
  if (error.message.includes('Rate limit exceeded')) {
    // Show user-friendly message
    showToast("You're posting too quickly. Please wait a moment.");
  }
}

Error Message Patterns

PatternMeaningUser Action
Rate limit exceeded: Too many requests in a short periodGeneric rate limitWait 60 seconds
Rate limit exceeded: Too many requests. Please try again later.Comments/messagesWait 60 seconds

Retry Strategy

async function executeWithRetry(
  action: () => Promise<any>,
  maxRetries = 3
) {
  for (let i = 0; i < maxRetries; i++) {
    try {
      return await action();
    } catch (error) {
      if (error.message.includes('Rate limit exceeded')) {
        if (i === maxRetries - 1) throw error;
        // Wait 60 seconds before retry
        await new Promise(resolve => setTimeout(resolve, 60000));
      } else {
        throw error; // Non-rate-limit errors shouldn't retry
      }
    }
  }
}

Configuring Rate Limits

Modifying Limits

To change rate limits, edit the guard function’s rateLimitCheck function: Example: Increase post creation limit to 20/minute
// In functions/posts-guard/src/main.js
async function rateLimitCheck(activity) {
  // ...
  
  // Change this line:
  if (row.activityCount >= 10) {
  // To:
  if (row.activityCount >= 20) {
    throw new Error(
      'Rate limit exceeded: Too many requests in a short period.'
    );
  }
  
  // ...
}

Deployment

After modifying rate limits:
  1. Test the function locally if possible
  2. Deploy the updated function:
    appwrite deploy function --functionId=6967383500111e42ec97
    
  3. Monitor logs for the first few minutes
  4. Update this documentation

Different Limits per Action

Some functions use action-specific limits:
async function rateLimitCheck(activity, customLimit) {
  const windowKey = Math.floor(Date.now() / 60000);
  const userActivity = await tablesDB.listRows({...});
  
  if (userActivity.rows.length === 0) {
    // Create new record
    return;
  }
  
  const limit = customLimit || 10; // Default to 10
  if (userActivity.rows[0].activityCount >= limit) {
    throw new Error('Rate limit exceeded');
  }
  
  // Increment counter
}

// Usage:
await rateLimitCheck('create_post', 10);
await rateLimitCheck('bulk_import', 5); // Stricter limit

Window Expiration

Rate limit records automatically become irrelevant after their time window passes: Old Window Example:
  • Current time: 2024-01-15 10:31:30 (windowKey: 28953451)
  • Existing record: windowKey: 28953450 (10:30:00 - 10:30:59)
  • Query won’t match old windowKey, so new record is created

Cleanup Strategy

Old records can be cleaned up with a scheduled function:
// Pseudo-code for cleanup function
export default async () => {
  const oneHourAgo = Math.floor((Date.now() - 3600000) / 60000);
  
  await tablesDB.deleteRows({
    databaseId: CRIC_TALK_DATABASE_ID,
    tableId: RATE_LIMIT_TABLE_ID,
    queries: [
      Query.lessThan('windowKey', oneHourAgo)
    ]
  });
};
Note: Appwrite doesn’t currently support bulk deletes via queries. Consider implementing this as a periodic maintenance task.

Atomic Operations

Rate limiting uses incrementRowColumn for atomic counter updates:
await tablesDB.incrementRowColumn({
  databaseId: CRIC_TALK_DATABASE_ID,
  tableId: RATE_LIMIT_TABLE_ID,
  rowId: row.$id,
  column: 'activityCount',
  value: 1,
});
Why Atomic:
  • Prevents race conditions when multiple requests arrive simultaneously
  • Ensures accurate counting even under high concurrency
  • No need for database locks or transactions

Best Practices

1. Check Before Operations

Always check rate limits BEFORE expensive operations:
async function createPost() {
  await rateLimitCheck('create_post'); // Check first
  
  // Only proceed if rate limit passed
  return await tablesDB.createRow({...});
}

2. Use Descriptive Activity Names

Make activity identifiers clear and consistent: ✅ Good:
await rateLimitCheck('create_post');
await rateLimitCheck('update_comment');
await rateLimitCheck('delete_room_message');
❌ Bad:
await rateLimitCheck('post');
await rateLimitCheck('comment_edit');
await rateLimitCheck('rm_msg_del');

3. Set Appropriate Limits

Consider the resource cost and user experience:
Resource CostRecommended LimitExample
High (images, complex validation)5-10/minPost creation
Medium (database writes)10-20/minRoom updates
Low (simple text, reads)30-60/minComments, messages

4. Provide Clear Error Messages

Include context in rate limit errors:
throw new Error(
  `Rate limit exceeded: You can only create ${limit} posts per minute. ` +
  `Please wait ${60 - secondsElapsed} seconds.`
);

5. Monitor Rate Limit Hits

Log when users hit limits for analytics:
if (row.activityCount >= limit) {
  console.log(`Rate limit hit: userId=${userId}, activity=${activity}`);
  throw new Error('Rate limit exceeded');
}

Bypassing Rate Limits

Admin Exceptions

To allow admins to bypass rate limits:
async function rateLimitCheck(activity) {
  // Check if user is admin
  const user = await users.get(userId);
  if (user.labels?.includes('admin')) {
    return; // Skip rate limiting for admins
  }
  
  // Regular rate limit check...
}

Per-User Custom Limits

Implement tiered limits based on user status:
async function getUserLimit(userId, defaultLimit) {
  const user = await users.get(userId);
  
  if (user.labels?.includes('premium')) {
    return defaultLimit * 2; // Premium users get 2x limit
  }
  if (user.labels?.includes('verified')) {
    return defaultLimit * 1.5; // Verified users get 1.5x limit
  }
  
  return defaultLimit;
}

// Usage:
const limit = await getUserLimit(userId, 10);
if (row.activityCount >= limit) {
  throw new Error('Rate limit exceeded');
}

Troubleshooting

Users Reporting False Rate Limits

Check:
  1. User’s device clock accuracy (affects window calculation)
  2. Multiple devices with same account (counters are shared)
  3. Browser/app caching old error messages
Debug Query:
const records = await tablesDB.listRows({
  databaseId: CRIC_TALK_DATABASE_ID,
  tableId: RATE_LIMIT_TABLE_ID,
  queries: [
    Query.equal('userId', userId),
    Query.equal('activity', 'create_post'),
    Query.orderDesc('$createdAt'),
    Query.limit(10)
  ]
});

Cleanup Not Working

Old records accumulating: Solution: Manually delete old records via Appwrite Console or create a maintenance function:
# Via Appwrite CLI
appwrite database deleteDocument \
  --databaseId=695761d00008fd927f78 \
  --collectionId=ratelimit \
  --documentId=<old_document_id>

Rate Limits Too Restrictive

Symptoms:
  • High rate limit error frequency in logs
  • User complaints about “can’t post” errors
  • Legitimate usage being blocked
Solutions:
  1. Increase the limit in the guard function
  2. Implement burst allowance (allow 15 in first 10 seconds, then 10/min)
  3. Add user feedback showing cooldown timer

Build docs developers (and LLMs) love