| 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".
|
| allowed-tools | Read, Write, Edit |
| version | 1.7.0 |
| license | MIT |
| author | Jeremy Longshore <jeremy@intentsolutions.io> |
| tags | ["saas","crm","attio"] |
| compatibility | Designed for Claude Code |
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;
const retryDate = new Date(retryAfter);
const waitMs = retryDate.getTime() - Date.now();
return Math.max(waitMs, 100);
}
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;
backoff = config. * .(, attempt);
jitter = .() * ;
delay = .(backoff + jitter, config.);
.(
+
);
( (r, delay));
} {
err;
}
}
}
();
}
Step 3: Queue-Based Throttling
Prevent 429s proactively by limiting concurrency and request rate:
import PQueue from "p-queue";
const attioQueue = new PQueue({
concurrency: 5,
interval: 1000,
intervalCap: 8,
});
async function throttledAttioCall<T>(
operation: () => Promise<T>
): Promise<T> {
return attioQueue.add(operation) as Promise<T>;
}
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();
if (now - this.windowStart > 10000) {
this.windowStart = now;
this.requestCount = 0;
}
this.requestCount++;
}
shouldThrottle(threshold = 0.8): boolean {
return this.requestCount > 50 * threshold;
}
getStats(): { requestsInWindow: number; windowAgeMs: number } {
return {
requestsInWindow: this.requestCount,
windowAgeMs: Date.now() - this.,
};
}
}
Step 5: Batch Operations to Reduce Request Count
for (const email of emails) {
await client.post("/objects/people/records/query", {
filter: { email_addresses: email },
limit: 1,
});
}
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;
private readonly resetMs = 30000;
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;
. = ;
result;
} (err) {
(err && err. === ) {
.++;
. = .();
(. >= .) {
. = ;
}
}
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.