Skip to content
Skillv1.0.0

attio-rate-limits

Handle Attio API rate limits with exponential backoff, queue-based throttling, and Retry-After header parsing. Trigger: "attio rate limit", "attio 429", "attio throttling", "attio retry", "attio backo

by jeremylongshore(0) 0 installs
Free
Sign in to install

Free account. Installing gives you the manifest plus copy-paste snippets.

See reviews

About

Imported from jeremylongshore/tons-of-skills-marketplace (plugins/saas-packs/attio-pack/skills/attio-rate-limits/SKILL.md). Install upstream with npx skills add jeremylongshore/tons-of-skills-marketplace --skill attio-rate-limits. Copyright stays with the author (MIT).

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;
    }
  }
}

Prerequisites

Confirm that you have an Attio workspace appropriate to the task, a dedicated non-production record or workspace for testing, and only the API token scopes or administrative access required by the procedure.

Output

Following this guide produces the Attio integration outcome for its topic—configuration, validation evidence, operational recovery, or a documented migration result. Record command output and relevant identifiers so a failed step is traceable.

Examples

Start with the smallest applicable command or code example in the relevant section, using a dedicated test record or workspace and non-production credentials. Confirm the expected response or validation result before applying the pattern to production.

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.

Use it

Copy one of these into your project. Installing also returns the manifest and these snippets.

yaml
targets:
  - https://api.opensmartroute.ai/api/v1/registry/jeremylongshore-tons-of-skills-marketplace-attio-rate-limits/manifest   # or paste the manifest below

Manifest

An Open Capability Manifest: the router reads it to know what this does, what it costs and when to pick it.

jeremylongshore-tons-of-skills-marketplace-attio-rate-limits.ocm.jsonjson
{
  "ocm": "1",
  "id": "jeremylongshore-tons-of-skills-marketplace-attio-rate-limits",
  "kind": "skill",
  "name": "attio-rate-limits",
  "description": "Handle Attio API rate limits with exponential backoff, queue-based throttling, and Retry-After header parsing. Trigger: \"attio rate limit\", \"attio 429\", \"attio throttling\", \"attio retry\", \"attio backoff\", \"attio too many requests\".",
  "publisher": "jeremylongshore",
  "version": "1.0.0",
  "capabilities": {
    "domains": [
      "coding"
    ],
    "tags": [
      "skill-md",
      "saas",
      "crm",
      "attio",
      "skills-sh"
    ],
    "languages": [
      "en"
    ]
  },
  "quality_prior": 0.6,
  "examples": [
    "Handle Attio API rate limits with exponential backoff, queue-based throttling, and Retry-After header parsing. Trigger: \"attio rate limit\", \"attio 429\", \"attio throttling\", \"attio retry\", \"attio backoff\", \"attio too many requests\"."
  ],
  "primary": false,
  "metadata": {
    "source": {
      "provider": "skills.sh",
      "repository": "https://github.com/jeremylongshore/tons-of-skills-marketplace",
      "path": "plugins/saas-packs/attio-pack/skills/attio-rate-limits/SKILL.md",
      "ref": "HEAD",
      "url": "https://github.com/jeremylongshore/tons-of-skills-marketplace/blob/HEAD/plugins/saas-packs/attio-pack/skills/attio-rate-limits/SKILL.md",
      "key": "jeremylongshore/tons-of-skills-marketplace/plugins/saas-packs/attio-pack/skills/attio-rate-limits/SKILL.md"
    },
    "compatibility": "Designed for Claude Code",
    "allowed_tools": [
      "Read,",
      "Write,",
      "Edit"
    ],
    "license": "MIT"
  },
  "instructions": "# Attio Rate Limits\n\n## Overview\n\nAttio 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.\n\n## Rate Limit Response\n\n```\nHTTP/1.1 429 Too Many Requests\nRetry-After: Sat, 22 Mar 2025 14:30:01 GMT\nContent-Type: application/json\n\n{\n  \"status_code\": 429,\n  \"type\": \"rate_limit_error\",\n  \"code\": \"rate_limit_exceeded\",\n  \"message\": \"Rate limit exceeded,",
  "cost": {
    "context_tokens": 1747
  }
}

Fetch it by URL: GET /api/v1/registry/jeremylongshore-tons-of-skills-marketplace-attio-rate-limits/manifest?version=1.0.0

Reviews

Star ratings from people who tried it. One review per account; edit yours any time.

No reviews yet. Install it, try it, and be the first to rate it.