Install any skill in seconds. Free to start, no credit card required.
Get Started Free →Implement Apollo.io rate limiting and backoff. Use when handling rate limits, implementing retry logic, or optimizing API request throughput. Trigger with phrases like "apollo rate limit", "apollo 429", "apollo throttling", "apollo backoff", "apollo request limits".
.claude/skills/jeremylongshore-apollo-rate-limits/SKILL.md| Test case | Without → With | Effect | Δ tokens | Δ turns |
|---|---|---|---|---|
| case-04 | ✗→✓ | ▲ Improved | 12% | 0% |
| case-05 | ✗→✓ | ▲ Improved | 30% | 0% |
| case-08 | ✗→✓ | ▲ Improved | 41% | 0% |
| case-09 | ✗→✓ | ▲ Improved | 83% | 0% |
| case-13 | ✗→✓ | ▲ Improved | 43% | 0% |
Implement robust rate limiting and backoff for the Apollo.io API. Apollo uses fixed-window rate limiting with per-endpoint limits. Unlike hourly quotas, Apollo limits are per minute with a burst limit per second. Exceeding them returns HTTP 429.
Apollo's official rate limits (as of 2025):
Endpoint Category | Limit/min | Burst/sec | Notes
----------------------------+-----------+-----------+-------------------------------
People Search | 100 | 10 | /mixed_people/api_search (free)
People Enrichment | 100 | 10 | /people/match (1 credit each)
Bulk People Enrichment | 10 | 2 | /people/bulk_match (up to 10/call)
Organization Search | 100 | 10 | /mixed_companies/search
Organization Enrichment | 100 | 10 | /organizations/enrich
Contacts CRUD | 100 | 10 | /contacts/*
Sequences | 100 | 10 | /emailer_campaigns/*
Deals | 100 | 10 | /opportunities/*Response headers on every successful call:
x-rate-limit-limit — max requests per windowx-rate-limit-remaining — requests remaining in current windowretry-after — seconds to wait (only on 429 responses)typescript// src/apollo/rate-limiter.ts export class SlidingWindowLimiter { private timestamps: number[] = []; constructor( private maxRequests: number = 100, private windowMs: number = 60_000, ) {} async acquire(): Promise<void> { const now = Date.now(); // Remove timestamps outside the window this.timestamps = this.timestamps.filter((t) => now - t < this.windowMs); if (this.timestamps.length >= this.maxRequests) { const oldestInWindow = this.timestamps[0]; const waitMs = this.windowMs - (now - oldestInWindow) + 100; console.warn(`[RateLimit] At capacity (${this.maxRequests}/${this.windowMs}ms). Waiting ${waitMs}ms`); await new Promise((r) => setTimeout(r, waitMs)); } this.timestamps.push(Date.now()); } get remaining(): number { const now = Date.now(); this.timestamps = this.timestamps.filter((t) => now - t < this.windowMs); return this.maxRequests - this.timestamps.length; } } // Create limiters per endpoint category export const limiters = { search: new SlidingWindowLimiter(100, 60_000), enrichment: new SlidingWindowLimiter(100, 60_000), bulkEnrichment: new SlidingWindowLimiter(10, 60_000), contacts: new SlidingWindowLimiter(100, 60_000), sequences: new SlidingWindowLimiter(100, 60_000), };
typescript// src/apollo/backoff.ts export async function withBackoff<T>( fn: () => Promise<T>, opts: { maxRetries?: number; baseMs?: number; maxMs?: number } = {}, ): Promise<T> { const { maxRetries = 5, baseMs = 1000, maxMs = 60_000 } = opts; for (let attempt = 0; attempt <= maxRetries; attempt++) { try { return await fn(); } catch (err: any) { const status = err.response?.status; if (status !== 429 && status < 500) throw err; if (attempt === maxRetries) throw err; // Prefer Retry-After header, fall back to exponential backoff const retryAfter = err.response?.headers?.['retry-after']; const delayMs = retryAfter ? parseInt(retryAfter, 10) * 1000 : Math.min(baseMs * 2 ** attempt + Math.random() * 500, maxMs); console.warn(`[Apollo] ${status} attempt ${attempt + 1}/${maxRetries + 1}, retry in ${Math.round(delayMs / 1000)}s`); await new Promise((r) => setTimeout(r, delayMs)); } } throw new Error('Unreachable'); }
typescript// src/apollo/queue.ts import PQueue from 'p-queue'; import { limiters } from './rate-limiter'; type EndpointCategory = keyof typeof limiters; const queues: Record<EndpointCategory, PQueue> = { search: new PQueue({ concurrency: 5, intervalCap: 10, interval: 1000 }), enrichment: new PQueue({ concurrency: 5, intervalCap: 10, interval: 1000 }), bulkEnrichment: new PQueue({ concurrency: 2, intervalCap: 2, interval: 1000 }), contacts: new PQueue({ concurrency: 5, intervalCap: 10, interval: 1000 }), sequences: new PQueue({ concurrency: 3, intervalCap: 5, interval: 1000 }), }; export async function queuedRequest<T>( category: EndpointCategory, fn: () => Promise<T>, ): Promise<T> { await limiters[category].acquire(); return queues[category].add(() => fn()) as Promise<T>; }
typescript// src/apollo/rate-monitor.ts import { AxiosInstance, AxiosResponse } from 'axios'; export function attachRateMonitor(client: AxiosInstance) { client.interceptors.response.use((response: AxiosResponse) => { const limit = response.headers['x-rate-limit-limit']; const remaining = response.headers['x-rate-limit-remaining']; if (limit && remaining) { const pct = Math.round(((parseInt(limit) - parseInt(remaining)) / parseInt(limit)) * 100); if (pct >= 80) { console.warn(`[Apollo] Rate limit ${pct}% used (${remaining}/${limit} remaining) on ${response.config.url}`); } } return response; }); }
retry-after headersPQueue-based request queue with per-second burst control| Scenario | Strategy | |----------|----------| | 429 with retry-after | Wait the specified seconds, then retry | | 429 without header | Exponential backoff: 1s, 2s, 4s, 8s, up to 60s | | Bulk enrichment limited | Use dedicated queue with 2/sec burst limit | | Near quota (>80%) | Log warning, defer non-critical requests |
typescriptimport { queuedRequest } from './apollo/queue'; import { withBackoff } from './apollo/backoff'; const domains = ['stripe.com', 'notion.so', 'linear.app', /* ... */]; const results = await Promise.all( domains.map((domain) => queuedRequest('search', () => withBackoff(() => client.post('/mixed_people/api_search', { q_organization_domains_list: [domain], per_page: 25, }), ), ), ), ); console.log(`Searched ${results.length} domains within rate limits`);
Proceed to apollo-security-basics for API security best practices.
| Case | Status | Duration (ms) | Turns | Tokens | Tool calls | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Without | With | Δ | Without | With | Δ | Without | With | Δ | Without | With | Δ | ||
case-01 | pass→pass | 18,813 | 14,200 | -25% | 1 | 1 | 0% | 4,288 | 5,369 | +25% | 0 | 0 | — |
case-02 | fail→fail | 18,712 | 15,199 | -19% | 1 | 1 | 0% | 4,215 | 5,623 | +33% | 0 | 0 | — |
case-03 | pass→pass | 15,914 | 12,090 | -24% | 1 | 1 | 0% | 3,317 | 4,848 | +46% | 0 | 0 | — |
case-04 | fail→pass | 13,446 | 4,526 | -66% | 1 | 1 | 0% | 2,724 | 3,046 | +12% | 0 | 0 | — |
case-05 | fail→pass | 11,055 | 3,964 | -64% | 1 | 1 | 0% | 2,251 | 2,923 | +30% | 0 | 0 | — |
case-06 | pass→pass | 12,098 | 8,470 | -30% | 1 | 1 | 0% | 2,309 | 3,689 | +60% | 0 | 0 | — |
case-07 | pass→pass | 14,479 | 9,642 | -33% | 1 | 1 | 0% | 3,259 | 4,094 | +26% | 0 | 0 | — |
case-08 | fail→pass | 14,820 | 10,633 | -28% | 1 | 1 | 0% | 3,046 | 4,290 | +41% | 0 | 0 | — |
case-09 | fail→pass | 7,881 | 2,436 | -69% | 1 | 1 | 0% | 1,383 | 2,535 | +83% | 0 | 0 | — |
case-10 | pass→pass | 12,496 | 7,370 | -41% | 1 | 1 | 0% | 2,650 | 3,602 | +36% | 0 | 0 | — |
case-11 | pass→pass | 7,073 | 1,858 | -74% | 1 | 1 | 0% | 1,471 | 2,428 | +65% | 0 | 0 | — |
case-12 | pass→pass | 11,304 | 6,387 | -43% | 1 | 1 | 0% | 2,122 | 3,231 | +52% | 0 | 0 | — |
case-13 | fail→pass | 11,217 | 4,985 | -56% | 1 | 1 | 0% | 2,121 | 3,028 | +43% | 0 | 0 | — |
case-14 | pass→pass | 14,318 | 9,803 | -32% | 1 | 1 | 0% | 2,789 | 3,907 | +40% | 0 | 0 | — |
case-15 | pass→pass | 13,119 | 3,913 | -70% | 1 | 1 | 0% | 2,788 | 2,927 | +5% | 0 | 0 | — |
case-16 | fail→pass | 3,149 | 2,327 | -26% | 1 | 1 | 0% | 523 | 2,461 | +371% | 0 | 0 | — |
case-17 | fail→pass | 8,792 | 3,185 | -64% | 1 | 1 | 0% | 1,368 | 2,545 | +86% | 0 | 0 | — |
case-18 | pass→pass | 9,570 | 1,880 | -80% | 1 | 1 | 0% | 1,493 | 2,334 | +56% | 0 | 0 | — |
case-19 | pass→pass | 11,705 | 5,987 | -49% | 1 | 1 | 0% | 1,974 | 3,281 | +66% | 0 | 0 | — |
case-20 | fail→pass | 10,308 | 2,110 | -80% | 1 | 1 | 0% | 1,783 | 2,452 | +38% | 0 | 0 | — |
case-21 | pass→pass | 5,471 | 3,028 | -45% | 1 | 1 | 0% | 999 | 2,502 | +150% | 0 | 0 | — |
case-22 | pass→pass | 10,860 | 5,244 | -52% | 1 | 1 | 0% | 2,211 | 3,135 | +42% | 0 | 0 | — |
case-23 | pass→pass | 9,610 | 7,313 | -24% | 1 | 1 | 0% | 1,648 | 3,492 | +112% | 0 | 0 | — |
case-24 | fail→fail | 10,642 | 7,249 | -32% | 1 | 1 | 0% | 2,055 | 3,551 | +73% | 0 | 0 | — |
case-25 | pass→pass | 9,245 | 5,320 | -42% | 1 | 1 | 0% | 1,764 | 2,983 | +69% | 0 | 0 | — |
DecimalAI ran this skill against gemini-3.6-flash twice over the same eval suite — once with the skill loaded and once without — and compared the two runs case by case. 25 cases were attempted. The headline lift of +32 percentage points is the difference between those two pass rates over the 25 comparable cases.
Without the skill loaded, the model failed this case. With it loaded, the same prompt on the same model passed. This is one improved case from the latest verified run; every case, including any that regressed, is in the table above.
Other measured skills in the registry, with their headline benchmark lift.