How Inbound Captação Webhooks and the AI Dispatcher Are Rate-Limited in DeskcommCRM

DeskcommCRM implements dual-layer rate limiting that protects public lead capture endpoints with per-IP and per-identifier throttling, while the AI dispatcher enforces per-tenant run caps using a shared Redis-backed counter system.

DeskcommCRM relies on precise rate-limiting mechanisms to prevent brute-force attacks on public webhooks and runaway AI processing costs. The open-source CRM isolates two critical traffic surfaces—inbound captação webhooks and the AI dispatcher—applying distinct quota strategies that are enforced through a common core utility in the codebase.

Inbound Captação Webhook Rate Limiting

Public lead capture endpoints are protected by the authRateLimited helper, which applies dual constraints before processing any payload.

Dual-Key Protection Strategy

The webhook handler applies two independent limits to prevent distributed abuse:

  • Per-IP rate limiting: Each client IP address is limited to 60 requests per 5-minute window, preventing a single source from brute-forcing lead submissions.
  • Per-identifier rate limiting: Each unique token or hashed email is limited to 5 requests per 5-minute window, protecting individual accounts from distributed enumeration attacks.

In app/api/v1/webhooks/in/[token]/route.ts, the handler resolves the client IP via clientIp() and hashes both the IP and the identifier using the opaque() utility before checking limits.

Implementation in the Webhook Handler

The route invokes authRateLimited from lib/auth/rate-limit.ts before executing business logic:

import { authRateLimited, AUTH_LIMITS } from "@/lib/auth/rate-limit";

export async function POST(req: Request) {
  const token = req.headers.get("x-token") ?? "";
  
  const limited = await authRateLimited(
    "capture",                     // action label
    token,                         // identifier (secret token in URL)
    AUTH_LIMITS.signup,           // limits for this surface
  );
  
  if (limited) {
    return Response.json(
      { error: "Rate limit exceeded" }, 
      { status: 429 }
    );
  }
  // ...process the lead
}

The authRateLimited function constructs Redis keys using the patterns auth:login:ip:<opaque-ip> and auth:login:id:<opaque-identifier>, then delegates the actual counter logic to the shared checkRateLimit utility.

AI Dispatcher Rate Limiting

The AI dispatcher governs background agent execution to prevent tenant-level resource exhaustion.

Per-Tenant Run Caps

Before scheduling an AI run, the dispatcher in lib/ai/dispatcher/index.ts validates quota availability:

import { checkRateLimit } from "./rate-limit";

const RATE_LIMIT_PER_MIN = 60;
const RATE_LIMIT_WINDOW_SEC = 60;

const rateResult = await checkRateLimit(
  `ai-runs:${orgId}`,
  RATE_LIMIT_PER_MIN,
  RATE_LIMIT_WINDOW_SEC,
);

if (!rateResult.allowed) {
  return { 
    status: "rate_limited", 
    count: rateResult.count, 
    limit: rateResult.limit 
  };
}

The default configuration allows 60 AI runs per minute per organization (orgId), with the threshold configurable via the RATE_LIMIT_PER_MIN environment variable.

Re-queuing and Backoff Behavior

When checkRateLimit returns allowed: false, the dispatcher returns a rate_limited status object. The surrounding retry logic in lib/event-log/dispatcher.ts interprets this status and re-queues the event with a 5-second delay (next_attempt_at = now() + 5s), creating automatic backoff without dropping the task.

The Shared Rate-Limiting Engine

Both surfaces rely on a single source of truth for counter operations to guarantee consistent semantics across the application.

Core checkRateLimit Implementation

The generic limiter resides in lib/ai/dispatcher/rate-limit.ts and exports the checkRateLimit function:

export async function checkRateLimit(
  key: string,
  limit: number,
  windowSec: number,
): Promise<{ allowed: boolean; count: number; limit: number }> {
  // Attempts Redis INCR + EXPIRE pipeline
  // Falls back to in-memory Map if Upstash Redis unavailable
}

Redis Key Structure and Fallback Logic

The function builds tenant-specific keys such as ai-runs:<orgId> for AI processing and auth:login:ip:<hash> for webhooks. It atomically increments counters and sets expiration windows using Redis pipelines. When the Upstash Redis client is not configured, the utility transparently falls back to an in-memory Map structure, ensuring that rate limiting remains functional in development or degraded production environments.

The result object returns the current count, the configured limit, and a boolean allowed flag, allowing callers to implement conditional logic without managing counter state.

Summary

  • Inbound captação webhooks are protected by authRateLimited in lib/auth/rate-limit.ts, enforcing 60 requests per 5 minutes per IP and 5 requests per 5 minutes per identifier.
  • AI dispatcher checks use checkRateLimit directly in lib/ai/dispatcher/index.ts, defaulting to 60 runs per minute per tenant with automatic 5-second re-queuing on limit violations.
  • Shared infrastructure in lib/ai/dispatcher/rate-limit.ts handles all counter logic, using Redis when available and falling back to in-memory storage when necessary.

Frequently Asked Questions

What are the default rate limits for inbound captação webhooks?

The default configuration allows 60 requests per 5-minute window per IP address and 5 requests per 5-minute window per unique identifier (hashed token or email). These values are defined in AUTH_LIMITS within lib/auth/rate-limit.ts.

How does the AI dispatcher handle rate-limited events?

When the per-tenant limit is exceeded, the dispatcher returns a rate_limited status. The event-log dispatcher in lib/event-log/dispatcher.ts catches this status and re-queues the event with a 5-second delay, enabling automatic retry without manual intervention.

Can rate limits be configured without modifying the source code?

Yes. The AI dispatcher reads the RATE_LIMIT_PER_MIN variable to determine per-tenant caps, while webhook limits are controlled through the AUTH_LIMITS configuration object. However, changing the hardcoded window durations (5 minutes for webhooks, 60 seconds for AI) requires editing the respective constants in the source files.

What happens if the Redis connection fails?

The checkRateLimit function in lib/ai/dispatcher/rate-limit.ts automatically falls back to an in-memory Map implementation when Redis is unavailable. This ensures that rate limiting continues to function locally, though counts will not persist across server restarts or scale horizontally across multiple instances without Redis.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →