clay-rate-limits

v2026.09.24

Handle Clay rate limits, webhook throttling, and credit pacing strategies. Use when hitting 429 errors, managing webhook submission rates, or optimizing throughput within Clay's plan limits. Trigger with phrases like "clay rate limit", "clay throttling", "clay 429", "clay slow", "clay records per hour".

GitHub
安装命令
npx skhub add jeremylongshore/clay-rate-limits
Markdown
SKILL.md

Clay Rate Limits

Overview

Clay enforces rate limits at the plan level, webhook level, and enrichment provider level. Understanding these limits prevents data loss, credit waste, and integration failures.

Prerequisites

  • Clay account with known plan tier
  • Webhook URL(s) for your tables
  • Understanding of your data volume requirements

Instructions

Step 1: Understand Clay Rate Limit Tiers

PlanRecords/HourWebhook LimitHTTP API ColumnsEnterprise API
FreeLimited50K lifetime per webhookNot availableNot available
StarterStandard50K lifetime per webhookNot availableNot available
Explorer400/hour50K lifetime per webhookNot availableNot available
ProUnlimited50K lifetime per webhookAvailableNot available
EnterpriseUnlimited50K lifetime per webhookAvailableAvailable

Key insight: The 50K webhook submission limit is per-webhook, not per-table. Once exhausted, create a new webhook on the same table.

Step 2: Implement Webhook Rate Limiting

// src/clay/rate-limiter.ts — respect Clay's plan-level rate limits
import PQueue from 'p-queue';

interface RateLimiterConfig {
  maxPerHour: number;     // Plan limit (e.g., 400 for Explorer)
  maxPerSecond: number;   // Practical burst limit
  webhookLimit: number;   // 50K per webhook lifetime
}

const PLAN_LIMITS: Record<string, RateLimiterConfig> = {
  explorer: { maxPerHour: 400, maxPerSecond: 2, webhookLimit: 50_000 },
  pro:      { maxPerHour: Infinity, maxPerSecond: 10, webhookLimit: 50_000 },
  enterprise: { maxPerHour: Infinity, maxPerSecond: 20, webhookLimit: 50_000 },
};

class ClayRateLimiter {
  private queue: PQueue;
  private submissionCount = 0;
  private hourlyCount = 0;
  private hourlyResetAt: Date;
  private config: RateLimiterConfig;

  constructor(plan: keyof typeof PLAN_LIMITS) {
    this.config = PLAN_LIMITS[plan];
    this.queue = new PQueue({
      concurrency: 1,
      interval: 1000,
      intervalCap: this.config.maxPerSecond,
    });
    this.hourlyResetAt = new Date(Date.now() + 3600_000);
  }

  async submit(webhookUrl: string, data: Record<string, unknown>): Promise<Response> {
    // Check webhook lifetime limit
    if (this.submissionCount >= this.config.webhookLimit) {
      throw new Error(
        `Webhook submission limit (${this.config.webhookLimit}) reached. Create a new webhook.`
      );
    }

    // Check hourly limit
    if (Date.now() > this.hourlyResetAt.getTime()) {
      this.hourlyCount = 0;
      this.hourlyResetAt = new Date(Date.now() + 3600_000);
    }
    if (this.hourlyCount >= this.config.maxPerHour) {
      const waitMs = this.hourlyResetAt.getTime() - Date.now();
      console.log(`Hourly limit reached. Waiting ${(waitMs / 1000).toFixed(0)}s...`);
      await new Promise(r => setTimeout(r, waitMs));
      this.hourlyCount = 0;
    }

    return this.queue.add(async () => {
      const res = await fetch(webhookUrl, {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify(data),
      });

      if (res.status === 429) {
        const retryAfter = parseInt(res.headers.get('Retry-After') || '60');
        console.log(`429 rate limited. Waiting ${retryAfter}s...`);
        await new Promise(r => setTimeout(r, retryAfter * 1000));
        return this.submit(webhookUrl, data); // Retry
      }

      this.submissionCount++;
      this.hourlyCount++;
      return res;
    });
  }

  getStats() {
    return {
      totalSubmissions: this.submissionCount,
      hourlyRemaining: this.config.maxPerHour - this.hourlyCount,
      webhookRemaining: this.config.webhookLimit - this.submissionCount,
    };
  }
}

Step 3: Handle 429 Responses with Backoff

// src/clay/backoff.ts
async function withClayBackoff<T>(
  operation: () => Promise<T>,
  maxRetries = 5
): Promise<T> {
  for (let attempt = 0; attempt <= maxRetries; attempt++) {
    try {
      return await operation();
    } catch (error: any) {
      if (attempt === maxRetries) throw error;

      // Clay returns 429 for plan-level rate limits
      const status = error.status || error.response?.status;
      if (status !== 429 && (status < 500 || status >= 600)) throw error;

      const baseDelay = 1000 * Math.pow(2, attempt); // 1s, 2s, 4s, 8s, 16s
      const jitter = Math.random() * 500;
      const delay = Math.min(baseDelay + jitter, 60_000); // Max 60s

      console.log(`Clay rate limited (attempt ${attempt + 1}). Retrying in ${(delay / 1000).toFixed(1)}s`);
      await new Promise(r => setTimeout(r, delay));
    }
  }
  throw new Error('Unreachable');
}

Step 4: Manage Webhook Lifecycle

// src/clay/webhook-manager.ts
interface WebhookState {
  url: string;
  submissionCount: number;
  createdAt: Date;
}

class WebhookManager {
  private webhooks: Map<string, WebhookState> = new Map();
  private readonly LIMIT = 50_000;
  private readonly WARN_THRESHOLD = 45_000;

  registerWebhook(tableId: string, url: string) {
    this.webhooks.set(tableId, { url, submissionCount: 0, createdAt: new Date() });
  }

  async getWebhookUrl(tableId: string): Promise<string> {
    const state = this.webhooks.get(tableId);
    if (!state) throw new Error(`No webhook registered for table ${tableId}`);

    if (state.submissionCount >= this.LIMIT) {
      throw new Error(
        `Webhook for table ${tableId} exhausted (${this.LIMIT} submissions). ` +
        `Create a new webhook in Clay UI: Table > + Add > Webhooks > Monitor webhook`
      );
    }

    if (state.submissionCount >= this.WARN_THRESHOLD) {
      console.warn(
        `Webhook for ${tableId} at ${state.submissionCount}/${this.LIMIT} submissions. ` +
        `Plan to create a replacement soon.`
      );
    }

    return state.url;
  }

  recordSubmission(tableId: string) {
    const state = this.webhooks.get(tableId);
    if (state) state.submissionCount++;
  }
}

Step 5: Enrichment Provider Rate Limits

Enrichment providers within Clay have their own limits. When using Clay's managed accounts, Clay handles throttling internally. When using your own API keys, you inherit the provider's rate limits:

ProviderTypical Rate LimitCredits per Lookup
Apollo100 req/min2 (own key: 0)
Clearbit600 req/min2-5 (own key: 0)
Hunter.io15 req/sec2 (own key: 0)
People Data Labs100 req/min3 (own key: 0)
Prospeo200 req/min2 (own key: 0)

Error Handling

ErrorCauseSolution
429 Too Many RequestsPlan-level hourly limitReduce submission rate, upgrade plan
Webhook silently stops50K submission limit hitCreate new webhook on same table
Enrichment stuckProvider rate limitWait or connect your own API key
Explorer 400/hr limitPlan restrictionQueue submissions, upgrade to Pro

Output

Emit a rate-control record with table/webhook scope, queue depth, request and provider response class, retry timing, remaining submission capacity, credit budget state, and terminal decision. Do not use fresh webhook URLs or higher concurrency to conceal a throttling failure; preserve the idempotency key and resume only through the approved scheduler.

Examples

Queue a staging batch at the defined concurrency, honor a provider’s retry guidance, and surface a warning before the table webhook reaches its limit. If the limit or budget guardrail is reached, stop new submissions, drain pending jobs safely, and schedule an approved replacement rather than resending rows.

Resources

Next Steps

For security configuration, see clay-security-basics.

发现
标签

此技能尚未发布标签。

版本
最新版本元数据

版本

v2026.09.24

发布时间

Sep 24, 2026

分类

未分类

许可证

MIT

源路径

skills/.curated/clay-rate-limits

默认分支

main

最新提交

e5a6c3b

Tree SHA

c2dc8e8