Agent Skills: Attio Rate Limits

|

UncategorizedID: jeremylongshore/claude-code-plugins-plus-skills/attio-rate-limits

Install this agent skill to your local

pnpm dlx add-skill https://github.com/jeremylongshore/claude-code-plugins-plus-skills/tree/HEAD/plugins/saas-packs/attio-pack/skills/attio-rate-limits

Skill Files

Browse the full folder contents for attio-rate-limits.

Download Skill

Loading file tree…

plugins/saas-packs/attio-pack/skills/attio-rate-limits/SKILL.md

Skill Metadata

Name
attio-rate-limits
Description
|

Attio Rate Limits

Overview

Attio uses a sliding window algorithm with a 10-second window. Rate limit scores are summed across all apps and access tokens hitting the API. When exceeded, you get HTTP 429 with a Retry-After header containing a date (usually the next second). Attio may temporarily reduce limits during incidents.

Rate Limit Response

HTTP/1.1 429 Too Many Requests
Retry-After: Sat, 22 Mar 2025 14:30:01 GMT
Content-Type: application/json

{
  "status_code": 429,
  "type": "rate_limit_error",
  "code": "rate_limit_exceeded",
  "message": "Rate limit exceeded, please try again later"
}

Key fact: The Retry-After header is a date string (not seconds). Parse it as a Date to calculate wait time.

Instructions

Step 1: Parse Retry-After Header

function parseRetryAfter(headers: Headers): number {
  const retryAfter = headers.get("Retry-After");
  if (!retryAfter) return 1000; // Default 1s

  // Attio sends a date string
  const retryDate = new Date(retryAfter);
  const waitMs = retryDate.getTime() - Date.now();
  return Math.max(waitMs, 100); // Minimum 100ms
}

Step 2: Exponential Backoff with Retry-After Awareness

import { AttioApiError } from "./client";

interface RetryConfig {
  maxRetries: number;
  baseMs: number;
  maxMs: number;
}

async function withRateLimitRetry<T>(
  operation: () => Promise<{ data: T; headers?: Headers }>,
  config: RetryConfig = { maxRetries: 5, baseMs: 1000, maxMs: 30000 }
): Promise<T> {
  for (let attempt = 0; attempt <= config.maxRetries; attempt++) {
    try {
      const result = await operation();
      return result.data;
    } catch (err) {
      if (attempt === config.maxRetries) throw err;

      if (err instanceof AttioApiError) {
        if (!err.retryable) throw err; // Only retry 429 and 5xx

        // Use Retry-After if available, otherwise exponential backoff
        const backoff = config.baseMs * Math.pow(2, attempt);
        const jitter = Math.random() * 500;
        const delay = Math.min(backoff + jitter, config.maxMs);

        console.warn(
          `Attio ${err.statusCode} on attempt ${attempt + 1}/${config.maxRetries}. ` +
          `Retrying in ${delay.toFixed(0)}ms...`
        );
        await new Promise((r) => setTimeout(r, delay));
      } else {
        throw err;
      }
    }
  }
  throw new Error("Unreachable");
}

Step 3: Queue-Based Throttling

Prevent 429s proactively by limiting concurrency and request rate:

import PQueue from "p-queue";

// Attio: sliding 10-second window. Stay well under the limit.
const attioQueue = new PQueue({
  concurrency: 5,           // Max parallel requests
  interval: 1000,           // 1 second interval
  intervalCap: 8,           // Max 8 requests per second
});

async function throttledAttioCall<T>(
  operation: () => Promise<T>
): Promise<T> {
  return attioQueue.add(operation) as Promise<T>;
}

// Usage
const results = await Promise.all(
  recordIds.map((id) =>
    throttledAttioCall(() =>
      client.get(`/objects/people/records/${id}`)
    )
  )
);

Step 4: Rate Limit Monitor

class AttioRateLimitMonitor {
  private windowStart = Date.now();
  private requestCount = 0;

  recordRequest(responseHeaders?: Headers): void {
    const now = Date.now();
    // Reset counter every 10 seconds (Attio's sliding window)
    if (now - this.windowStart > 10000) {
      this.windowStart = now;
      this.requestCount = 0;
    }
    this.requestCount++;
  }

  shouldThrottle(threshold = 0.8): boolean {
    // Conservative: throttle at 80% of observed capacity
    return this.requestCount > 50 * threshold; // Adjust 50 based on your limit
  }

  getStats(): { requestsInWindow: number; windowAgeMs: number } {
    return {
      requestsInWindow: this.requestCount,
      windowAgeMs: Date.now() - this.windowStart,
    };
  }
}

Step 5: Batch Operations to Reduce Request Count

// Instead of N individual GET calls, use the query endpoint (1 POST)
// BAD: N requests
for (const email of emails) {
  await client.post("/objects/people/records/query", {
    filter: { email_addresses: email },
    limit: 1,
  });
}

// GOOD: 1 request with $in filter
const results = await client.post("/objects/people/records/query", {
  filter: {
    email_addresses: {
      email_address: { $in: emails },
    },
  },
  limit: emails.length,
});

Step 6: Circuit Breaker for Sustained Rate Limiting

class AttioCircuitBreaker {
  private failures = 0;
  private lastFailure = 0;
  private state: "closed" | "open" | "half-open" = "closed";
  private readonly threshold = 5;       // Open after 5 consecutive 429s
  private readonly resetMs = 30000;     // Try again after 30s

  async execute<T>(operation: () => Promise<T>): Promise<T> {
    if (this.state === "open") {
      if (Date.now() - this.lastFailure > this.resetMs) {
        this.state = "half-open";
      } else {
        throw new Error("Circuit open: Attio rate limited. Retry after 30s.");
      }
    }

    try {
      const result = await operation();
      this.failures = 0;
      this.state = "closed";
      return result;
    } catch (err) {
      if (err instanceof AttioApiError && err.statusCode === 429) {
        this.failures++;
        this.lastFailure = Date.now();
        if (this.failures >= this.threshold) {
          this.state = "open";
        }
      }
      throw err;
    }
  }
}

Error Handling

| Symptom | Cause | Solution | |---------|-------|----------| | Burst of 429s on startup | No throttling | Add PQueue with intervalCap | | 429s during bulk import | Too many parallel requests | Reduce concurrency, batch with query | | Intermittent 429s | Multiple apps sharing limit | Coordinate rate across apps | | 429s after long silence | Attio reduced limit during incident | Check status.attio.com, honor Retry-After |

Resources

Next Steps

For security best practices, see attio-security-basics.