Skip to main content
Fluxer implements rate limiting to prevent abuse and ensure fair resource allocation. The system uses the GCRA (Generic Cell Rate Algorithm) for smooth, accurate rate limiting with minimal memory overhead.

Rate Limit Algorithm

Fluxer uses the GCRA algorithm, which provides:
  • Smooth rate limiting - No burst allowances that can be abused
  • Memory efficient - Requires only 2 values per bucket
  • Accurate - Precise to the millisecond
  • Distributed - Works across multiple instances via shared cache

How GCRA Works

GCRA tracks:
  1. Theoretical Arrival Time (TAT) - When the next request should be allowed
  2. Current Time - When the request was made
A request is allowed if: current_time >= TAT - window_ms

Rate Limit Buckets

Rate limits are organized into buckets based on the resource being accessed:

Bucket Types

Applied across all endpoints for an IP or user:
'auth:register'      // Registration attempts
'auth:login'         // Login attempts
'auth:forgot'        // Password reset requests

Common Rate Limits

Authentication

EndpointLimitWindowBucket
Register1010 secondsauth:register
Login1010 secondsauth:login
Login MFA51 minuteauth:login:mfa
Forgot Password51 minuteauth:forgot
Reset Password101 minuteauth:reset
Verify Email101 minuteauth:verify
Logout2010 secondsauth:logout

Channels

OperationLimitWindowBucket
Get Channel10010 secondschannel:read::channel_id
Send Message2010 secondschannel:message:create::channel_id
Edit Message2010 secondschannel:message:update::channel_id
Delete Message2010 secondschannel:message:delete::channel_id
Bulk Delete1010 secondschannel:message:bulk_delete::channel_id
Add Reaction3010 secondschannel:reactions::channel_id
Typing Indicator2010 secondschannel:typing::channel_id
Get Messages10010 secondschannel:messages:read::channel_id
Pin Message2010 secondschannel:pins::channel_id

Voice

OperationLimitWindowBucket
Get Call6010 secondschannel:call:get::channel_id
Update Call1010 secondschannel:call:update::channel_id
Ring510 secondschannel:call:ring::channel_id
Stop Ringing2010 secondschannel:call:stop_ringing::channel_id

Multi-Factor Authentication

OperationLimitWindowBucket
Enable SMS MFA101 minutemfa:sms:enable
Disable SMS MFA101 minutemfa:sms:disable
WebAuthn Registration101 minutemfa:webauthn:register
WebAuthn List4010 secondsmfa:webauthn:list
WebAuthn Delete101 minutemfa:webauthn:delete

Phone Verification

OperationLimitWindowBucket
Send Verification51 minutephone:send_verification
Verify Code101 minutephone:verify_code
Add Phone101 minutephone:add
Remove Phone101 minutephone:remove

Rate Limit Headers

Fluxer includes rate limit information in HTTP response headers:
HTTP/1.1 200 OK
X-RateLimit-Limit: 20
X-RateLimit-Remaining: 15
X-RateLimit-Reset: 1640995200000
X-RateLimit-Bucket: channel:message:create::123456789

Header Descriptions

HeaderDescription
X-RateLimit-LimitMaximum number of requests in the window
X-RateLimit-RemainingRequests remaining in the current window
X-RateLimit-ResetTimestamp (ms) when the limit resets
X-RateLimit-BucketBucket identifier for this rate limit
Retry-AfterSeconds until the rate limit resets (on 429)

Rate Limit Errors

When a rate limit is exceeded, the API returns:
{
  "code": "RATE_LIMITED",
  "message": "You are being rate limited",
  "retry_after": 3.5,
  "global": false
}
HTTP Status: 429 Too Many Requests

Error Fields

code
string
Error code: RATE_LIMITED
message
string
Human-readable error message
retry_after
number
Seconds to wait before retrying
global
boolean
Whether this is a global rate limit (affects all endpoints)

Implementation

Service Architecture

import { RateLimitService } from '@fluxer/rate_limit';
import { InMemoryCacheService } from '@fluxer/rate_limit';

// Create cache service
const cacheService = new InMemoryCacheService();

// Create rate limit service
const rateLimitService = new RateLimitService(cacheService, {
  globalWindowMs: 1000 // Global window: 1 second
});

Checking Rate Limits

// Check a rate limit
const result = await rateLimitService.checkLimit({
  identifier: 'user:123456',
  maxAttempts: 20,
  windowMs: 10000 // 10 seconds
});

if (!result.allowed) {
  console.log(`Rate limited. Retry after ${result.retryAfterMs}ms`);
}

Bucket-Based Limits

// Check bucket-specific limit
const result = await rateLimitService.checkBucketLimit(
  'channel:message:create::123456',
  {
    limit: 20,
    windowMs: 10000
  }
);

Global Rate Limits

// Check global limit (applies across all endpoints)
const result = await rateLimitService.checkGlobalLimit(
  'user:123456',
  50 // 50 requests per second
);

Resetting Limits

// Reset rate limit for an identifier (admin only)
await rateLimitService.resetLimit('user:123456');

Rate Limit Configuration

Route Configuration

Define rate limits for API routes:
import { ms } from 'itty-time';

export const ChannelRateLimitConfigs = {
  CHANNEL_MESSAGE_CREATE: {
    bucket: 'channel:message:create::channel_id',
    config: { 
      limit: 20, 
      windowMs: ms('10 seconds') 
    }
  },
  
  CHANNEL_MESSAGE_DELETE: {
    bucket: 'channel:message:delete::channel_id',
    config: { 
      limit: 20, 
      windowMs: ms('10 seconds') 
    }
  }
} as const;

Middleware

Apply rate limiting middleware:
import { rateLimitMiddleware } from '@fluxer/api';
import { ChannelRateLimitConfigs } from '@fluxer/api/rate_limit_configs';

app.post(
  '/channels/:channelId/messages',
  rateLimitMiddleware(ChannelRateLimitConfigs.CHANNEL_MESSAGE_CREATE),
  async (c) => {
    // Handle message creation
  }
);

Best Practices

Respect Rate Limits

Always check rate limit headers and implement exponential backoff when hitting limits.

Use Bucket Identifiers

Include resource IDs in bucket names to isolate rate limits per channel/guild/user.

Cache Service

Use a distributed cache (Redis) in production for rate limiting across multiple API instances.

Monitor Usage

Track rate limit hits to identify potential abuse or legitimate high-traffic patterns.

Handling Rate Limits

Exponential Backoff

async function sendMessageWithRetry(
  channelId: string,
  content: string,
  maxRetries = 3
) {
  let retries = 0;
  
  while (retries < maxRetries) {
    try {
      return await sendMessage(channelId, content);
    } catch (error) {
      if (error.code === 'RATE_LIMITED') {
        const delay = error.retry_after * 1000;
        await new Promise(resolve => setTimeout(resolve, delay));
        retries++;
      } else {
        throw error;
      }
    }
  }
  
  throw new Error('Max retries exceeded');
}

Queue-Based Approach

class RateLimitedQueue {
  private queue: Array<() => Promise<void>> = [];
  private processing = false;
  
  async add(fn: () => Promise<void>) {
    this.queue.push(fn);
    if (!this.processing) {
      await this.process();
    }
  }
  
  private async process() {
    this.processing = true;
    
    while (this.queue.length > 0) {
      const fn = this.queue.shift()!;
      try {
        await fn();
      } catch (error) {
        if (error.code === 'RATE_LIMITED') {
          // Re-queue and wait
          this.queue.unshift(fn);
          await new Promise(resolve => 
            setTimeout(resolve, error.retry_after * 1000)
          );
        }
      }
    }
    
    this.processing = false;
  }
}

Special Cases

Global Rate Limits

Global rate limits apply across all API endpoints for a user/IP:
// Global limit: 50 requests per second across all endpoints
const result = await rateLimitService.checkGlobalLimit(
  'user:123456',
  50
);

Slowmode

Channel-specific slowmode is a special rate limit:
{
  "code": "SLOWMODE_RATE_LIMITED",
  "message": "You are sending messages too quickly",
  "retry_after": 5.0
}
Users with BYPASS_SLOWMODE permission are exempt.

Configuration Options

interface RateLimitServiceOptions {
  globalWindowMs?: number;      // Global window duration (default: 1000ms)
  getCurrentTimeMs?: () => number; // Custom time source
}

Cache Implementation

In-Memory Cache

import { InMemoryCacheService } from '@fluxer/rate_limit';

const cache = new InMemoryCacheService();
In-memory cache is not recommended for production multi-instance deployments. Use Redis or another distributed cache.

Redis Cache (Production)

import { RedisCacheService } from '@fluxer/cache';

const cache = new RedisCacheService({
  host: 'localhost',
  port: 6379
});

Build docs developers (and LLMs) love